nautobot 2.3.15b1__py3-none-any.whl → 2.4.0__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.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (785) hide show
  1. nautobot/__init__.py +15 -0
  2. nautobot/apps/__init__.py +1 -1
  3. nautobot/apps/api.py +8 -10
  4. nautobot/apps/change_logging.py +2 -2
  5. nautobot/apps/choices.py +4 -4
  6. nautobot/apps/config.py +32 -3
  7. nautobot/apps/events.py +19 -0
  8. nautobot/apps/exceptions.py +0 -2
  9. nautobot/apps/factory.py +2 -2
  10. nautobot/apps/filters.py +1 -1
  11. nautobot/apps/forms.py +20 -20
  12. nautobot/apps/graphql.py +2 -2
  13. nautobot/apps/jobs.py +8 -8
  14. nautobot/apps/models.py +19 -19
  15. nautobot/apps/tables.py +1 -1
  16. nautobot/apps/testing.py +10 -10
  17. nautobot/apps/ui.py +44 -9
  18. nautobot/apps/utils.py +7 -15
  19. nautobot/apps/views.py +8 -6
  20. nautobot/circuits/api/serializers.py +1 -0
  21. nautobot/circuits/api/views.py +4 -8
  22. nautobot/circuits/navigation.py +0 -57
  23. nautobot/circuits/templates/circuits/circuit_create.html +1 -7
  24. nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -71
  25. nautobot/circuits/templates/circuits/inc/circuit_termination.html +6 -64
  26. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +40 -0
  27. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +26 -0
  28. nautobot/circuits/templates/circuits/provider_retrieve.html +0 -76
  29. nautobot/circuits/tests/integration/test_relationships.py +33 -24
  30. nautobot/circuits/tests/test_filters.py +4 -8
  31. nautobot/circuits/views.py +143 -26
  32. nautobot/cloud/api/views.py +6 -10
  33. nautobot/cloud/factory.py +4 -1
  34. nautobot/cloud/models.py +1 -1
  35. nautobot/cloud/tests/test_filters.py +5 -4
  36. nautobot/cloud/views.py +0 -16
  37. nautobot/core/api/constants.py +11 -0
  38. nautobot/core/api/fields.py +5 -5
  39. nautobot/core/api/filter_backends.py +3 -9
  40. nautobot/core/api/metadata.py +28 -256
  41. nautobot/core/api/pagination.py +3 -2
  42. nautobot/core/api/renderers.py +3 -0
  43. nautobot/core/api/schema.py +13 -2
  44. nautobot/core/api/serializers.py +54 -268
  45. nautobot/core/api/urls.py +3 -4
  46. nautobot/core/api/utils.py +0 -62
  47. nautobot/core/api/views.py +102 -159
  48. nautobot/core/apps/__init__.py +22 -575
  49. nautobot/core/celery/__init__.py +13 -0
  50. nautobot/core/celery/schedulers.py +48 -3
  51. nautobot/core/choices.py +2 -2
  52. nautobot/core/cli/__init__.py +8 -0
  53. nautobot/core/constants.py +7 -0
  54. nautobot/core/events/__init__.py +116 -0
  55. nautobot/core/events/base.py +27 -0
  56. nautobot/core/events/exceptions.py +10 -0
  57. nautobot/core/events/redis_broker.py +48 -0
  58. nautobot/core/events/syslog_broker.py +19 -0
  59. nautobot/core/exceptions.py +0 -6
  60. nautobot/core/filters.py +19 -16
  61. nautobot/core/forms/__init__.py +19 -19
  62. nautobot/core/forms/fields.py +62 -14
  63. nautobot/core/forms/forms.py +33 -2
  64. nautobot/core/forms/utils.py +2 -1
  65. nautobot/core/graphql/schema.py +3 -1
  66. nautobot/core/graphql/types.py +1 -1
  67. nautobot/core/jobs/__init__.py +28 -7
  68. nautobot/core/jobs/bulk_actions.py +248 -0
  69. nautobot/core/jobs/cleanup.py +2 -2
  70. nautobot/core/jobs/groups.py +1 -1
  71. nautobot/core/management/commands/generate_test_data.py +21 -0
  72. nautobot/core/management/commands/validate_models.py +1 -1
  73. nautobot/core/middleware.py +16 -0
  74. nautobot/core/models/__init__.py +1 -1
  75. nautobot/core/models/fields.py +11 -7
  76. nautobot/core/models/query_functions.py +2 -2
  77. nautobot/core/models/tree_queries.py +2 -2
  78. nautobot/core/settings.py +71 -4
  79. nautobot/core/settings.yaml +107 -0
  80. nautobot/core/tables.py +15 -51
  81. nautobot/core/tasks.py +1 -1
  82. nautobot/core/templates/about.html +67 -0
  83. nautobot/core/templates/components/button/default.html +7 -0
  84. nautobot/core/templates/components/button/dropdown.html +20 -0
  85. nautobot/core/templates/components/layout/one_over_two.html +19 -0
  86. nautobot/core/templates/components/layout/two_over_one.html +19 -0
  87. nautobot/core/templates/components/panel/body_content_data_table.html +27 -0
  88. nautobot/core/templates/components/panel/body_content_objects_table.html +4 -0
  89. nautobot/core/templates/components/panel/body_content_tags.html +6 -0
  90. nautobot/core/templates/components/panel/body_content_text.html +12 -0
  91. nautobot/core/templates/components/panel/body_wrapper_generic.html +3 -0
  92. nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +3 -0
  93. nautobot/core/templates/components/panel/body_wrapper_table.html +3 -0
  94. nautobot/core/templates/components/panel/footer_contacts_table.html +20 -0
  95. nautobot/core/templates/components/panel/footer_content_table.html +14 -0
  96. nautobot/core/templates/components/panel/grouping_toggle.html +14 -0
  97. nautobot/core/templates/components/panel/header_extra_content_table.html +3 -0
  98. nautobot/core/templates/components/panel/panel.html +16 -0
  99. nautobot/core/templates/components/panel/stats_panel_body.html +8 -0
  100. nautobot/core/templates/components/tab/content_wrapper.html +3 -0
  101. nautobot/core/templates/components/tab/label_wrapper.html +5 -0
  102. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +3 -0
  103. nautobot/core/templates/generic/object_retrieve.html +28 -17
  104. nautobot/core/templates/inc/computed_fields/panel_data.html +4 -7
  105. nautobot/core/templates/inc/custom_fields/panel.html +2 -2
  106. nautobot/core/templates/inc/custom_fields/panel_data.html +4 -7
  107. nautobot/core/templates/inc/footer.html +1 -0
  108. nautobot/core/templates/inc/media.html +3 -0
  109. nautobot/core/templates/inc/nav_menu.html +2 -1
  110. nautobot/core/templates/inc/relationships_panel.html +1 -1
  111. nautobot/core/templates/inc/tenancy_form_panel.html +9 -0
  112. nautobot/core/templates/inc/tenant_table_row.html +11 -0
  113. nautobot/core/templates/nautobot_config.py.j2 +16 -0
  114. nautobot/core/templates/panel_table.html +12 -0
  115. nautobot/core/templates/utilities/render_jinja2.html +117 -0
  116. nautobot/core/templates/utilities/templatetags/tag.html +1 -1
  117. nautobot/core/templates/utilities/theme_preview.html +7 -0
  118. nautobot/core/templatetags/helpers.py +104 -6
  119. nautobot/core/templatetags/ui_framework.py +40 -0
  120. nautobot/core/testing/__init__.py +8 -8
  121. nautobot/core/testing/api.py +187 -137
  122. nautobot/core/testing/context.py +18 -0
  123. nautobot/core/testing/filters.py +44 -34
  124. nautobot/core/testing/forms.py +2 -0
  125. nautobot/core/testing/views.py +68 -146
  126. nautobot/core/tests/integration/test_app_home.py +0 -1
  127. nautobot/core/tests/integration/test_app_navbar.py +0 -1
  128. nautobot/core/tests/integration/test_filters.py +0 -2
  129. nautobot/core/tests/integration/test_home.py +0 -1
  130. nautobot/core/tests/integration/test_navbar.py +0 -1
  131. nautobot/core/tests/integration/test_view_authentication.py +2 -1
  132. nautobot/core/tests/nautobot_config.py +198 -0
  133. nautobot/core/tests/runner.py +3 -3
  134. nautobot/core/tests/test_api.py +154 -176
  135. nautobot/core/tests/test_events.py +214 -0
  136. nautobot/core/tests/test_forms.py +1 -0
  137. nautobot/core/tests/test_jinja_filters.py +1 -0
  138. nautobot/core/tests/test_jobs.py +387 -14
  139. nautobot/core/tests/test_navigations.py +7 -241
  140. nautobot/core/tests/test_settings_schema.py +7 -0
  141. nautobot/core/tests/test_tables.py +100 -0
  142. nautobot/core/tests/test_templatetags_helpers.py +16 -0
  143. nautobot/core/tests/test_ui.py +150 -0
  144. nautobot/core/tests/test_utils.py +55 -18
  145. nautobot/core/tests/test_views.py +153 -5
  146. nautobot/core/ui/__init__.py +0 -0
  147. nautobot/core/ui/base.py +11 -0
  148. nautobot/core/ui/choices.py +44 -0
  149. nautobot/core/ui/homepage.py +167 -0
  150. nautobot/core/ui/nav.py +280 -0
  151. nautobot/core/ui/object_detail.py +1855 -0
  152. nautobot/core/ui/utils.py +36 -0
  153. nautobot/core/urls.py +15 -0
  154. nautobot/core/utils/config.py +30 -3
  155. nautobot/core/utils/lookup.py +12 -2
  156. nautobot/core/utils/querysets.py +64 -0
  157. nautobot/core/utils/requests.py +24 -9
  158. nautobot/core/views/__init__.py +48 -1
  159. nautobot/core/views/generic.py +82 -177
  160. nautobot/core/views/mixins.py +98 -38
  161. nautobot/core/views/paginator.py +8 -5
  162. nautobot/core/views/renderers.py +9 -9
  163. nautobot/core/views/utils.py +11 -0
  164. nautobot/core/wsgi.py +3 -3
  165. nautobot/dcim/api/serializers.py +82 -189
  166. nautobot/dcim/api/urls.py +5 -0
  167. nautobot/dcim/api/views.py +57 -110
  168. nautobot/dcim/apps.py +1 -0
  169. nautobot/dcim/choices.py +28 -0
  170. nautobot/dcim/factory.py +58 -0
  171. nautobot/dcim/filters/__init__.py +204 -2
  172. nautobot/dcim/forms.py +221 -9
  173. nautobot/dcim/graphql/types.py +2 -2
  174. nautobot/dcim/migrations/0063_interfacevdcassignment_virtualdevicecontext_and_more.py +165 -0
  175. nautobot/dcim/migrations/0064_virtualdevicecontext_status_data_migration.py +28 -0
  176. nautobot/dcim/migrations/0065_controller_capabilities_and_more.py +29 -0
  177. nautobot/dcim/migrations/0066_controllermanageddevicegroup_radio_profiles_and_more.py +33 -0
  178. nautobot/dcim/migrations/0067_controllermanageddevicegroup_tenant.py +25 -0
  179. nautobot/dcim/models/__init__.py +5 -1
  180. nautobot/dcim/models/device_component_templates.py +2 -2
  181. nautobot/dcim/models/device_components.py +22 -20
  182. nautobot/dcim/models/devices.py +181 -3
  183. nautobot/dcim/models/locations.py +3 -3
  184. nautobot/dcim/models/power.py +6 -5
  185. nautobot/dcim/models/racks.py +6 -6
  186. nautobot/dcim/navigation.py +25 -224
  187. nautobot/dcim/signals.py +44 -0
  188. nautobot/dcim/tables/__init__.py +5 -3
  189. nautobot/dcim/tables/devices.py +103 -7
  190. nautobot/dcim/tables/devicetypes.py +2 -2
  191. nautobot/dcim/tables/racks.py +1 -1
  192. nautobot/dcim/templates/dcim/controller/base.html +10 -0
  193. nautobot/dcim/templates/dcim/controller_create.html +2 -7
  194. nautobot/dcim/templates/dcim/controller_retrieve.html +6 -10
  195. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +25 -0
  196. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +68 -0
  197. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +51 -0
  198. nautobot/dcim/templates/dcim/device/base.html +6 -42
  199. nautobot/dcim/templates/dcim/device/wireless.html +73 -0
  200. nautobot/dcim/templates/dcim/device.html +4 -10
  201. nautobot/dcim/templates/dcim/device_edit.html +36 -37
  202. nautobot/dcim/templates/dcim/interface.html +1 -0
  203. nautobot/dcim/templates/dcim/interface_edit.html +1 -0
  204. nautobot/dcim/templates/dcim/location.html +1 -9
  205. nautobot/dcim/templates/dcim/location_edit.html +1 -7
  206. nautobot/dcim/templates/dcim/locationtype.html +0 -107
  207. nautobot/dcim/templates/dcim/locationtype_retrieve.html +8 -0
  208. nautobot/dcim/templates/dcim/rack.html +1 -9
  209. nautobot/dcim/templates/dcim/rack_edit.html +1 -7
  210. nautobot/dcim/templates/dcim/rackreservation.html +1 -9
  211. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +68 -0
  212. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +28 -0
  213. nautobot/dcim/tests/integration/test_controller.py +62 -0
  214. nautobot/dcim/tests/integration/test_controller_managed_device_group.py +71 -0
  215. nautobot/dcim/tests/test_api.py +188 -64
  216. nautobot/dcim/tests/test_filters.py +172 -76
  217. nautobot/dcim/tests/test_graphql.py +52 -0
  218. nautobot/dcim/tests/test_jobs.py +118 -0
  219. nautobot/dcim/tests/test_models.py +159 -5
  220. nautobot/dcim/tests/test_signals.py +1 -0
  221. nautobot/dcim/tests/test_views.py +118 -88
  222. nautobot/dcim/urls.py +72 -27
  223. nautobot/dcim/utils.py +2 -2
  224. nautobot/dcim/views.py +357 -62
  225. nautobot/extras/api/customfields.py +2 -2
  226. nautobot/extras/api/serializers.py +111 -87
  227. nautobot/extras/api/urls.py +4 -0
  228. nautobot/extras/api/views.py +93 -35
  229. nautobot/extras/choices.py +13 -0
  230. nautobot/extras/constants.py +2 -1
  231. nautobot/extras/context_managers.py +23 -6
  232. nautobot/extras/datasources/git.py +4 -1
  233. nautobot/extras/factory.py +27 -0
  234. nautobot/extras/filters/__init__.py +66 -5
  235. nautobot/extras/forms/base.py +2 -2
  236. nautobot/extras/forms/forms.py +262 -59
  237. nautobot/extras/forms/mixins.py +3 -3
  238. nautobot/extras/graphql/types.py +25 -1
  239. nautobot/extras/health_checks.py +1 -2
  240. nautobot/extras/jobs.py +114 -20
  241. nautobot/extras/management/__init__.py +1 -0
  242. nautobot/extras/management/commands/runjob.py +7 -79
  243. nautobot/extras/management/commands/runjob_with_job_result.py +46 -0
  244. nautobot/extras/management/utils.py +87 -0
  245. nautobot/extras/managers.py +3 -1
  246. nautobot/extras/migrations/0018_joblog_data_migration.py +7 -9
  247. nautobot/extras/migrations/0117_create_job_queue_model.py +129 -0
  248. nautobot/extras/migrations/0118_task_queue_to_job_queue_migration.py +78 -0
  249. nautobot/extras/migrations/0119_remove_task_queues_from_job_and_queue_from_scheduled_job.py +28 -0
  250. nautobot/extras/migrations/0120_job_is_singleton_job_is_singleton_override.py +22 -0
  251. nautobot/extras/migrations/0121_alter_team_contacts.py +17 -0
  252. nautobot/extras/models/__init__.py +5 -1
  253. nautobot/extras/models/change_logging.py +7 -3
  254. nautobot/extras/models/contacts.py +1 -1
  255. nautobot/extras/models/customfields.py +12 -11
  256. nautobot/extras/models/groups.py +11 -9
  257. nautobot/extras/models/jobs.py +237 -37
  258. nautobot/extras/models/models.py +2 -2
  259. nautobot/extras/models/relationships.py +69 -1
  260. nautobot/extras/models/secrets.py +5 -0
  261. nautobot/extras/navigation.py +20 -262
  262. nautobot/extras/plugins/__init__.py +54 -19
  263. nautobot/extras/plugins/marketplace_manifest.yml +455 -0
  264. nautobot/extras/plugins/tables.py +16 -14
  265. nautobot/extras/plugins/urls.py +1 -0
  266. nautobot/extras/plugins/views.py +104 -61
  267. nautobot/extras/registry.py +1 -1
  268. nautobot/extras/secrets/__init__.py +2 -2
  269. nautobot/extras/signals.py +39 -1
  270. nautobot/extras/tables.py +42 -6
  271. nautobot/extras/templates/extras/dynamicgroup.html +1 -9
  272. nautobot/extras/templates/extras/externalintegration_retrieve.html +0 -47
  273. nautobot/extras/templates/extras/inc/tags_panel.html +1 -5
  274. nautobot/extras/templates/extras/job_bulk_edit.html +2 -1
  275. nautobot/extras/templates/extras/job_detail.html +52 -6
  276. nautobot/extras/templates/extras/job_edit.html +6 -2
  277. nautobot/extras/templates/extras/job_list.html +2 -7
  278. nautobot/extras/templates/extras/jobqueue_retrieve.html +36 -0
  279. nautobot/extras/templates/extras/marketplace.html +296 -0
  280. nautobot/extras/templates/extras/plugin_detail.html +32 -15
  281. nautobot/extras/templates/extras/plugins_list.html +35 -1
  282. nautobot/extras/templates/extras/plugins_tiles.html +90 -0
  283. nautobot/extras/templates/extras/role_retrieve.html +16 -0
  284. nautobot/extras/templates/extras/secret.html +0 -65
  285. nautobot/extras/templates/extras/secret_check.js +16 -0
  286. nautobot/extras/templates/extras/secret_create.html +114 -0
  287. nautobot/extras/templates/extras/secret_edit.html +1 -114
  288. nautobot/extras/templates/extras/secretsgroup_edit.html +1 -1
  289. nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -0
  290. nautobot/extras/templatetags/job_buttons.py +5 -4
  291. nautobot/extras/templatetags/plugins.py +69 -6
  292. nautobot/extras/test_jobs/api_test_job.py +1 -1
  293. nautobot/extras/test_jobs/atomic_transaction.py +2 -2
  294. nautobot/extras/test_jobs/dry_run.py +1 -1
  295. nautobot/extras/test_jobs/fail.py +5 -5
  296. nautobot/extras/test_jobs/file_output.py +1 -1
  297. nautobot/extras/test_jobs/file_upload_fail.py +1 -1
  298. nautobot/extras/test_jobs/file_upload_pass.py +1 -1
  299. nautobot/extras/test_jobs/ipaddress_vars.py +3 -1
  300. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +1 -1
  301. nautobot/extras/test_jobs/location_with_custom_field.py +1 -1
  302. nautobot/extras/test_jobs/log_redaction.py +1 -1
  303. nautobot/extras/test_jobs/log_skip_db_logging.py +1 -1
  304. nautobot/extras/test_jobs/modify_db.py +1 -1
  305. nautobot/extras/test_jobs/object_var_optional.py +1 -1
  306. nautobot/extras/test_jobs/object_var_required.py +1 -1
  307. nautobot/extras/test_jobs/object_vars.py +1 -1
  308. nautobot/extras/test_jobs/pass.py +3 -3
  309. nautobot/extras/test_jobs/profiling.py +1 -1
  310. nautobot/extras/test_jobs/relative_import.py +3 -3
  311. nautobot/extras/test_jobs/singleton.py +16 -0
  312. nautobot/extras/test_jobs/soft_time_limit_greater_than_time_limit.py +1 -1
  313. nautobot/extras/test_jobs/task_queues.py +1 -1
  314. nautobot/extras/tests/integration/test_plugin_banner.py +0 -2
  315. nautobot/extras/tests/test_api.py +157 -55
  316. nautobot/extras/tests/test_context_managers.py +4 -1
  317. nautobot/extras/tests/test_customfields.py +1 -1
  318. nautobot/extras/tests/test_datasources.py +2 -1
  319. nautobot/extras/tests/test_dynamicgroups.py +1 -1
  320. nautobot/extras/tests/test_filters.py +219 -535
  321. nautobot/extras/tests/test_forms.py +20 -1
  322. nautobot/extras/tests/test_job_variables.py +73 -152
  323. nautobot/extras/tests/test_jobs.py +192 -62
  324. nautobot/extras/tests/test_models.py +71 -16
  325. nautobot/extras/tests/test_plugins.py +62 -9
  326. nautobot/extras/tests/test_relationships.py +124 -10
  327. nautobot/extras/tests/test_utils.py +23 -2
  328. nautobot/extras/tests/test_views.py +162 -161
  329. nautobot/extras/tests/test_webhooks.py +2 -1
  330. nautobot/extras/urls.py +2 -20
  331. nautobot/extras/utils.py +119 -4
  332. nautobot/extras/views.py +188 -141
  333. nautobot/extras/webhooks.py +5 -2
  334. nautobot/ipam/api/fields.py +3 -3
  335. nautobot/ipam/api/serializers.py +44 -137
  336. nautobot/ipam/api/views.py +68 -110
  337. nautobot/ipam/factory.py +1 -1
  338. nautobot/ipam/filters.py +3 -2
  339. nautobot/ipam/models.py +10 -12
  340. nautobot/ipam/navigation.py +0 -90
  341. nautobot/ipam/querysets.py +2 -2
  342. nautobot/ipam/tables.py +3 -1
  343. nautobot/ipam/templates/ipam/ipaddress.html +1 -9
  344. nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +1 -7
  345. nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -7
  346. nautobot/ipam/templates/ipam/prefix.html +1 -9
  347. nautobot/ipam/templates/ipam/prefix_edit.html +1 -7
  348. nautobot/ipam/templates/ipam/routetarget.html +0 -28
  349. nautobot/ipam/templates/ipam/vlan.html +1 -9
  350. nautobot/ipam/templates/ipam/vlan_edit.html +1 -7
  351. nautobot/ipam/templates/ipam/vrf.html +0 -47
  352. nautobot/ipam/templates/ipam/vrf_edit.html +1 -7
  353. nautobot/ipam/tests/test_api.py +19 -6
  354. nautobot/ipam/tests/test_filters.py +39 -119
  355. nautobot/ipam/tests/test_forms.py +49 -47
  356. nautobot/ipam/tests/test_migrations.py +30 -30
  357. nautobot/ipam/tests/test_models.py +56 -36
  358. nautobot/ipam/tests/test_querysets.py +14 -0
  359. nautobot/ipam/tests/test_views.py +3 -0
  360. nautobot/ipam/urls.py +3 -69
  361. nautobot/ipam/utils/__init__.py +16 -10
  362. nautobot/ipam/views.py +91 -162
  363. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +1 -1
  364. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +1 -1
  365. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +40 -2
  366. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +1 -1
  367. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +1 -1
  368. nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +1 -1
  369. nautobot/project-static/css/base.css +38 -3
  370. nautobot/project-static/docs/404.html +463 -19
  371. nautobot/project-static/docs/apps/index.html +463 -19
  372. nautobot/project-static/docs/apps/nautobot-apps.html +464 -21
  373. nautobot/project-static/docs/assets/_mkdocstrings.css +25 -1
  374. nautobot/project-static/docs/assets/extra.css +5 -1
  375. nautobot/project-static/docs/assets/javascripts/{bundle.83f73b43.min.js → bundle.88dd0f4e.min.js} +2 -2
  376. nautobot/project-static/docs/assets/javascripts/{bundle.83f73b43.min.js.map → bundle.88dd0f4e.min.js.map} +2 -2
  377. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +479 -25
  378. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +476 -22
  379. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +792 -291
  380. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +507 -33
  381. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +512 -36
  382. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +473 -22
  383. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +469 -20
  384. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +499 -35
  385. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +9883 -0
  386. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +525 -77
  387. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +548 -53
  388. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +672 -96
  389. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1033 -180
  390. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +526 -51
  391. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +876 -190
  392. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +957 -237
  393. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +477 -23
  394. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +488 -30
  395. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +663 -101
  396. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +949 -481
  397. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +6427 -1236
  398. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +476 -22
  399. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +879 -346
  400. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +830 -173
  401. nautobot/project-static/docs/development/apps/api/configuration-view.html +463 -19
  402. nautobot/project-static/docs/development/apps/api/database-backend-config.html +463 -19
  403. nautobot/project-static/docs/development/apps/api/models/django-admin.html +463 -19
  404. nautobot/project-static/docs/development/apps/api/models/global-search.html +463 -19
  405. nautobot/project-static/docs/development/apps/api/models/graphql.html +463 -19
  406. nautobot/project-static/docs/development/apps/api/models/index.html +463 -19
  407. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +463 -19
  408. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +463 -19
  409. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +463 -19
  410. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +463 -19
  411. nautobot/project-static/docs/development/apps/api/platform-features/index.html +463 -19
  412. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +463 -19
  413. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +463 -19
  414. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +463 -19
  415. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +463 -19
  416. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +463 -19
  417. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +463 -19
  418. nautobot/project-static/docs/development/apps/api/prometheus.html +463 -19
  419. nautobot/project-static/docs/development/apps/api/setup.html +467 -155
  420. nautobot/project-static/docs/development/apps/api/testing.html +463 -19
  421. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +463 -19
  422. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +463 -19
  423. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +463 -19
  424. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +463 -19
  425. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +743 -130
  426. nautobot/project-static/docs/development/apps/api/views/base-template.html +463 -19
  427. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +463 -19
  428. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +463 -19
  429. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +463 -19
  430. nautobot/project-static/docs/development/apps/api/views/index.html +465 -20
  431. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +467 -19
  432. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +493 -19
  433. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +463 -19
  434. nautobot/project-static/docs/development/apps/api/views/notes.html +463 -19
  435. nautobot/project-static/docs/development/apps/api/views/rest-api.html +469 -21
  436. nautobot/project-static/docs/development/apps/api/views/urls.html +463 -19
  437. nautobot/project-static/docs/development/apps/index.html +463 -19
  438. nautobot/project-static/docs/development/apps/migration/code-updates.html +464 -52
  439. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +464 -20
  440. nautobot/project-static/docs/development/apps/migration/from-v1.html +463 -19
  441. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +463 -19
  442. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +463 -19
  443. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +463 -19
  444. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +466 -22
  445. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +9261 -0
  446. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +9375 -0
  447. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +9671 -0
  448. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +9559 -0
  449. nautobot/project-static/docs/development/apps/porting-from-netbox.html +466 -22
  450. nautobot/project-static/docs/development/core/application-registry.html +463 -19
  451. nautobot/project-static/docs/development/core/best-practices.html +463 -19
  452. nautobot/project-static/docs/development/core/bootstrap-ui.html +463 -19
  453. nautobot/project-static/docs/development/core/caching.html +463 -19
  454. nautobot/project-static/docs/development/core/controllers.html +465 -19
  455. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +491 -90
  456. nautobot/project-static/docs/development/core/generic-views.html +463 -19
  457. nautobot/project-static/docs/development/core/getting-started.html +541 -129
  458. nautobot/project-static/docs/development/core/homepage.html +474 -30
  459. nautobot/project-static/docs/development/core/index.html +463 -19
  460. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +9754 -0
  461. nautobot/project-static/docs/development/core/model-checklist.html +473 -27
  462. nautobot/project-static/docs/development/core/model-features.html +463 -19
  463. nautobot/project-static/docs/development/core/natural-keys.html +463 -19
  464. nautobot/project-static/docs/development/core/navigation-menu.html +480 -26
  465. nautobot/project-static/docs/development/core/release-checklist.html +480 -48
  466. nautobot/project-static/docs/development/core/role-internals.html +463 -19
  467. nautobot/project-static/docs/development/core/settings.html +463 -19
  468. nautobot/project-static/docs/development/core/style-guide.html +466 -22
  469. nautobot/project-static/docs/development/core/templates.html +473 -22
  470. nautobot/project-static/docs/development/core/testing.html +463 -19
  471. nautobot/project-static/docs/development/core/ui-component-framework.html +11116 -0
  472. nautobot/project-static/docs/development/core/user-preferences.html +466 -22
  473. nautobot/project-static/docs/development/index.html +463 -19
  474. nautobot/project-static/docs/development/jobs/index.html +501 -21
  475. nautobot/project-static/docs/development/jobs/migration/from-v1.html +463 -19
  476. nautobot/project-static/docs/index.html +471 -38
  477. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_edit.png +0 -0
  478. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_edit_button.png +0 -0
  479. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_list_nav.png +0 -0
  480. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_list_view.png +0 -0
  481. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue.png +0 -0
  482. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue_add.png +0 -0
  483. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_queue_config.png +0 -0
  484. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_completed.png +0 -0
  485. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_nav.png +0 -0
  486. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_result_pending.png +0 -0
  487. nautobot/project-static/docs/media/development/core/kubernetes/k8s_job_run_form.png +0 -0
  488. nautobot/project-static/docs/media/development/core/kubernetes/k8s_nautobot_login.png +0 -0
  489. nautobot/project-static/docs/media/development/core/kubernetes/k8s_run_job.png +0 -0
  490. nautobot/project-static/docs/media/development/core/kubernetes/k8s_run_scheduled_job_form.png +0 -0
  491. nautobot/project-static/docs/media/development/core/kubernetes/k8s_scheduled_job_result.png +0 -0
  492. nautobot/project-static/docs/media/development/core/ui-component-framework/basic-panel-layout.png +0 -0
  493. nautobot/project-static/docs/media/development/core/ui-component-framework/button-example.png +0 -0
  494. nautobot/project-static/docs/media/development/core/ui-component-framework/buttons-example.png +0 -0
  495. nautobot/project-static/docs/media/development/core/ui-component-framework/cluster-type-before-after-example.png +0 -0
  496. nautobot/project-static/docs/media/development/core/ui-component-framework/dropdown-button-example.png +0 -0
  497. nautobot/project-static/docs/media/development/core/ui-component-framework/grouped-key-value-table-panel-example-1.png +0 -0
  498. nautobot/project-static/docs/media/development/core/ui-component-framework/grouped-key-value-table-panel-example-2.png +0 -0
  499. nautobot/project-static/docs/media/development/core/ui-component-framework/object-fields-panel-example.png +0 -0
  500. nautobot/project-static/docs/media/development/core/ui-component-framework/object-fields-panel-example_2.png +0 -0
  501. nautobot/project-static/docs/media/development/core/ui-component-framework/stats-panel-example-code.png +0 -0
  502. nautobot/project-static/docs/media/development/core/ui-component-framework/stats-panel-example.png +0 -0
  503. nautobot/project-static/docs/media/development/core/ui-component-framework/table-panels-family.png +0 -0
  504. nautobot/project-static/docs/media/development/core/ui-component-framework/text-panels-family.png +0 -0
  505. nautobot/project-static/docs/media/development/core/ui-component-framework/ui-framework-example.png +0 -0
  506. nautobot/project-static/docs/media/models/virtual_device_context_overview.drawio +73 -0
  507. nautobot/project-static/docs/media/models/virtual_device_context_overview.png +0 -0
  508. nautobot/project-static/docs/models/dcim/virtualdevicecontext.html +14 -0
  509. nautobot/project-static/docs/models/extras/jobqueue.html +14 -0
  510. nautobot/project-static/docs/models/wireless/radioprofile.html +14 -0
  511. nautobot/project-static/docs/models/wireless/supporteddatarate.html +14 -0
  512. nautobot/project-static/docs/models/wireless/wirelessnetwork.html +14 -0
  513. nautobot/project-static/docs/objects.inv +0 -0
  514. nautobot/project-static/docs/overview/application_stack.html +469 -23
  515. nautobot/project-static/docs/overview/design_philosophy.html +463 -19
  516. nautobot/project-static/docs/release-notes/index.html +485 -22
  517. nautobot/project-static/docs/release-notes/version-1.0.html +651 -208
  518. nautobot/project-static/docs/release-notes/version-1.1.html +648 -205
  519. nautobot/project-static/docs/release-notes/version-1.2.html +723 -280
  520. nautobot/project-static/docs/release-notes/version-1.3.html +749 -306
  521. nautobot/project-static/docs/release-notes/version-1.4.html +834 -392
  522. nautobot/project-static/docs/release-notes/version-1.5.html +1022 -581
  523. nautobot/project-static/docs/release-notes/version-1.6.html +942 -518
  524. nautobot/project-static/docs/release-notes/version-2.0.html +945 -504
  525. nautobot/project-static/docs/release-notes/version-2.1.html +780 -339
  526. nautobot/project-static/docs/release-notes/version-2.2.html +773 -332
  527. nautobot/project-static/docs/release-notes/version-2.3.html +1142 -448
  528. nautobot/project-static/docs/release-notes/version-2.4.html +10323 -0
  529. nautobot/project-static/docs/requirements.txt +2 -2
  530. nautobot/project-static/docs/search/search_index.json +1 -1
  531. nautobot/project-static/docs/sitemap.xml +342 -270
  532. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  533. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +463 -19
  534. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +463 -19
  535. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +463 -19
  536. nautobot/project-static/docs/user-guide/administration/configuration/index.html +475 -32
  537. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +463 -19
  538. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +894 -180
  539. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +463 -19
  540. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +463 -19
  541. nautobot/project-static/docs/user-guide/administration/guides/docker.html +476 -29
  542. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +463 -19
  543. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +463 -19
  544. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +463 -19
  545. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +463 -19
  546. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +463 -19
  547. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +465 -21
  548. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +463 -19
  549. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +463 -19
  550. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +463 -19
  551. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +483 -23
  552. nautobot/project-static/docs/user-guide/administration/installation/index.html +468 -20
  553. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +464 -20
  554. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +463 -19
  555. nautobot/project-static/docs/user-guide/administration/installation/services.html +463 -19
  556. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +463 -19
  557. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +484 -41
  558. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +477 -66
  559. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +477 -66
  560. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +463 -19
  561. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +463 -19
  562. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +463 -19
  563. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +463 -19
  564. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +463 -19
  565. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +466 -23
  566. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +463 -19
  567. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-nautobot-app-location.yaml +0 -16
  568. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +463 -19
  569. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +469 -21
  570. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +463 -19
  571. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +463 -19
  572. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +463 -19
  573. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +463 -19
  574. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +463 -19
  575. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +463 -19
  576. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +463 -19
  577. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +463 -19
  578. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +463 -19
  579. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +463 -19
  580. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +463 -19
  581. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +463 -19
  582. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +463 -19
  583. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +463 -19
  584. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +463 -19
  585. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +463 -19
  586. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +463 -19
  587. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +499 -20
  588. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +489 -22
  589. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +463 -19
  590. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +463 -19
  591. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +463 -19
  592. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +463 -19
  593. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +463 -19
  594. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +463 -19
  595. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +463 -19
  596. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +463 -19
  597. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +463 -19
  598. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +463 -19
  599. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +463 -19
  600. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +463 -19
  601. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +463 -19
  602. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +463 -19
  603. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +463 -19
  604. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +463 -19
  605. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +463 -19
  606. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +463 -19
  607. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +463 -19
  608. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +463 -19
  609. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +463 -19
  610. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +463 -19
  611. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +463 -19
  612. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +463 -19
  613. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +463 -19
  614. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +463 -19
  615. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +463 -19
  616. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +463 -19
  617. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +463 -19
  618. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +463 -19
  619. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +463 -19
  620. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +463 -19
  621. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +463 -19
  622. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +463 -19
  623. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +9375 -0
  624. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +470 -30
  625. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +463 -19
  626. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +463 -19
  627. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +463 -19
  628. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +463 -19
  629. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +463 -19
  630. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +463 -19
  631. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +463 -19
  632. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +463 -19
  633. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +463 -19
  634. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +463 -19
  635. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +463 -19
  636. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +463 -19
  637. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +466 -22
  638. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +463 -19
  639. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +463 -19
  640. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +463 -19
  641. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +463 -19
  642. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +463 -19
  643. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +463 -19
  644. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +466 -22
  645. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +9313 -0
  646. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +9217 -0
  647. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +9211 -0
  648. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +9277 -0
  649. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +463 -19
  650. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +463 -19
  651. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +463 -19
  652. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +463 -19
  653. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +463 -19
  654. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +463 -19
  655. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +463 -19
  656. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +463 -19
  657. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +463 -19
  658. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +463 -19
  659. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +463 -19
  660. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +468 -22
  661. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +463 -19
  662. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/central-mode.png +0 -0
  663. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-add.png +0 -0
  664. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-create-1.png +0 -0
  665. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/device-group-create-2.png +0 -0
  666. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/radio-profile-add.png +0 -0
  667. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/radio-profile-create.png +0 -0
  668. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/supported-data-rate-add.png +0 -0
  669. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/supported-data-rate-create.png +0 -0
  670. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-add.png +0 -0
  671. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-create-1.png +0 -0
  672. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-controller-create-2.png +0 -0
  673. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-network-add.png +0 -0
  674. nautobot/project-static/docs/user-guide/feature-guides/images/wireless/wireless-network-create.png +0 -0
  675. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +463 -19
  676. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +463 -19
  677. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +466 -22
  678. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +9444 -0
  679. nautobot/project-static/docs/user-guide/index.html +463 -19
  680. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +466 -22
  681. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +467 -23
  682. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +463 -19
  683. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +463 -19
  684. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +463 -19
  685. nautobot/project-static/docs/user-guide/platform-functionality/events.html +9617 -0
  686. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +466 -22
  687. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +463 -19
  688. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +463 -19
  689. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +463 -19
  690. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +463 -19
  691. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +463 -19
  692. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +472 -23
  693. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +466 -22
  694. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +466 -22
  695. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +463 -19
  696. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +9224 -0
  697. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +9722 -0
  698. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +466 -22
  699. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +463 -19
  700. nautobot/project-static/docs/user-guide/platform-functionality/note.html +463 -19
  701. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +463 -19
  702. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +467 -23
  703. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +9292 -0
  704. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +463 -19
  705. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +511 -40
  706. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +494 -23
  707. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +463 -19
  708. nautobot/project-static/docs/user-guide/platform-functionality/role.html +463 -19
  709. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +463 -19
  710. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +463 -19
  711. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +466 -22
  712. nautobot/project-static/docs/user-guide/platform-functionality/status.html +463 -19
  713. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +463 -19
  714. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +531 -56
  715. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +463 -19
  716. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +463 -19
  717. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +463 -19
  718. nautobot/project-static/img/jinja_logo.svg +97 -0
  719. nautobot/project-static/js/forms.js +6 -1
  720. nautobot/project-static/js/nav_menu.js +2 -1
  721. nautobot/tenancy/api/serializers.py +0 -2
  722. nautobot/tenancy/api/views.py +9 -13
  723. nautobot/tenancy/factory.py +1 -1
  724. nautobot/tenancy/navigation.py +0 -29
  725. nautobot/tenancy/templates/tenancy/tenant.html +4 -91
  726. nautobot/tenancy/tests/test_filters.py +29 -134
  727. nautobot/tenancy/views.py +35 -24
  728. nautobot/users/admin.py +3 -1
  729. nautobot/users/api/serializers.py +4 -4
  730. nautobot/users/api/views.py +3 -3
  731. nautobot/users/forms.py +19 -0
  732. nautobot/users/templates/users/preferences.html +22 -0
  733. nautobot/users/tests/test_filters.py +1 -19
  734. nautobot/users/tests/test_views.py +57 -0
  735. nautobot/users/utils.py +8 -0
  736. nautobot/users/views.py +48 -11
  737. nautobot/virtualization/api/serializers.py +4 -4
  738. nautobot/virtualization/api/views.py +5 -24
  739. nautobot/virtualization/filters.py +1 -2
  740. nautobot/virtualization/models.py +1 -1
  741. nautobot/virtualization/navigation.py +0 -48
  742. nautobot/virtualization/tables.py +2 -2
  743. nautobot/virtualization/templates/virtualization/cluster_edit.html +1 -7
  744. nautobot/virtualization/templates/virtualization/clustertype.html +0 -39
  745. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -9
  746. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +2 -8
  747. nautobot/virtualization/tests/test_filters.py +57 -166
  748. nautobot/virtualization/views.py +18 -15
  749. nautobot/wireless/__init__.py +0 -0
  750. nautobot/wireless/api/__init__.py +0 -0
  751. nautobot/wireless/api/serializers.py +44 -0
  752. nautobot/wireless/api/urls.py +20 -0
  753. nautobot/wireless/api/views.py +34 -0
  754. nautobot/wireless/apps.py +8 -0
  755. nautobot/wireless/choices.py +345 -0
  756. nautobot/wireless/factory.py +138 -0
  757. nautobot/wireless/filters.py +167 -0
  758. nautobot/wireless/forms.py +283 -0
  759. nautobot/wireless/homepage.py +19 -0
  760. nautobot/wireless/migrations/0001_initial.py +223 -0
  761. nautobot/wireless/migrations/__init__.py +0 -0
  762. nautobot/wireless/models.py +207 -0
  763. nautobot/wireless/navigation.py +105 -0
  764. nautobot/wireless/tables.py +244 -0
  765. nautobot/wireless/templates/wireless/radioprofile_retrieve.html +81 -0
  766. nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +26 -0
  767. nautobot/wireless/templates/wireless/wirelessnetwork_create.html +88 -0
  768. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +56 -0
  769. nautobot/wireless/tests/__init__.py +0 -0
  770. nautobot/wireless/tests/integration/__init__.py +0 -0
  771. nautobot/wireless/tests/integration/test_radio_profile.py +42 -0
  772. nautobot/wireless/tests/test_api.py +247 -0
  773. nautobot/wireless/tests/test_filters.py +82 -0
  774. nautobot/wireless/tests/test_models.py +22 -0
  775. nautobot/wireless/tests/test_views.py +378 -0
  776. nautobot/wireless/urls.py +13 -0
  777. nautobot/wireless/views.py +119 -0
  778. {nautobot-2.3.15b1.dist-info → nautobot-2.4.0.dist-info}/METADATA +11 -13
  779. {nautobot-2.3.15b1.dist-info → nautobot-2.4.0.dist-info}/RECORD +783 -613
  780. {nautobot-2.3.15b1.dist-info → nautobot-2.4.0.dist-info}/WHEEL +1 -1
  781. nautobot/core/fixtures/user-data.json +0 -59
  782. nautobot/core/utils/navigation.py +0 -54
  783. {nautobot-2.3.15b1.dist-info → nautobot-2.4.0.dist-info}/LICENSE.txt +0 -0
  784. {nautobot-2.3.15b1.dist-info → nautobot-2.4.0.dist-info}/NOTICE +0 -0
  785. {nautobot-2.3.15b1.dist-info → nautobot-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  from datetime import timedelta
2
+ import json
2
3
  from unittest import mock
3
4
  import urllib.parse
4
5
  import uuid
5
6
 
6
- from django.conf import settings
7
7
  from django.contrib.auth import get_user_model
8
8
  from django.contrib.contenttypes.models import ContentType
9
9
  from django.core.exceptions import ValidationError
@@ -14,15 +14,17 @@ from django.utils import timezone
14
14
  from django.utils.html import escape, format_html
15
15
 
16
16
  from nautobot.circuits.models import Circuit
17
+ from nautobot.core.celery import NautobotKombuJSONEncoder
17
18
  from nautobot.core.choices import ColorChoices
18
19
  from nautobot.core.models.fields import slugify_dashes_to_underscores
20
+ from nautobot.core.models.utils import serialize_object_v2
19
21
  from nautobot.core.templatetags.helpers import bettertitle
20
22
  from nautobot.core.testing import extract_form_failures, extract_page_body, ModelViewTestCase, TestCase, ViewTestCases
23
+ from nautobot.core.testing.context import load_event_broker_override_settings
21
24
  from nautobot.core.testing.utils import disable_warnings, get_deletable_objects, post_data
22
25
  from nautobot.core.utils.permissions import get_permission_for_model
23
26
  from nautobot.dcim.models import (
24
27
  ConsolePort,
25
- Controller,
26
28
  Device,
27
29
  DeviceType,
28
30
  Interface,
@@ -30,11 +32,11 @@ from nautobot.dcim.models import (
30
32
  LocationType,
31
33
  Manufacturer,
32
34
  )
33
- from nautobot.dcim.tests import test_views
34
35
  from nautobot.extras.choices import (
35
36
  CustomFieldTypeChoices,
36
37
  DynamicGroupTypeChoices,
37
38
  JobExecutionType,
39
+ JobQueueTypeChoices,
38
40
  LogLevelChoices,
39
41
  MetadataTypeDataTypeChoices,
40
42
  ObjectChangeActionChoices,
@@ -59,6 +61,7 @@ from nautobot.extras.models import (
59
61
  Job,
60
62
  JobButton,
61
63
  JobLogEntry,
64
+ JobQueue,
62
65
  JobResult,
63
66
  MetadataType,
64
67
  Note,
@@ -83,7 +86,7 @@ from nautobot.extras.templatetags.job_buttons import NO_CONFIRM_BUTTON
83
86
  from nautobot.extras.tests.constants import BIG_GRAPHQL_DEVICE_QUERY
84
87
  from nautobot.extras.tests.test_relationships import RequiredRelationshipTestMixin
85
88
  from nautobot.extras.utils import RoleModelsQuery, TaggableClassesQuery
86
- from nautobot.ipam.models import IPAddress, Prefix, VLAN, VLANGroup
89
+ from nautobot.ipam.models import IPAddress, Prefix, VLAN, VLANGroup, VRF
87
90
  from nautobot.tenancy.models import Tenant
88
91
  from nautobot.users.models import ObjectPermission
89
92
 
@@ -908,6 +911,30 @@ class DynamicGroupTestCase(
908
911
  self.form_data["content_type"] = instance.content_type.pk # Content-type is not editable after creation
909
912
  super().test_edit_object_with_constrained_permission()
910
913
 
914
+ def test_edit_object_with_content_type_ipam_prefix(self):
915
+ """Assert bug fix #6526: `Error when defining Dynamic Group of Prefixes using `present_in_vrf_id` filter`"""
916
+ content_type = ContentType.objects.get_for_model(Prefix)
917
+ instance = DynamicGroup.objects.create(name="DG Ipam|Prefix", content_type=content_type)
918
+ vrf_instance = VRF.objects.first()
919
+ data = self.form_data.copy()
920
+ data.update(
921
+ {
922
+ "name": "DG Ipam|Prefix",
923
+ "content_type": content_type.pk,
924
+ "filter-present_in_vrf_id": vrf_instance.id,
925
+ "tenant": None,
926
+ "tags": [],
927
+ }
928
+ )
929
+ self.add_permissions("extras.change_dynamicgroup")
930
+ request = {
931
+ "path": self._get_url("edit", instance),
932
+ "data": post_data(data),
933
+ }
934
+ self.assertHttpStatus(self.client.post(**request), 302)
935
+ instance.refresh_from_db()
936
+ self.assertEqual(instance.filter["present_in_vrf_id"], str(vrf_instance.id))
937
+
911
938
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
912
939
  def test_edit_saved_filter(self):
913
940
  """Test that editing a filter works using the edit view."""
@@ -1691,21 +1718,21 @@ class ScheduledJobTestCase(
1691
1718
  user = User.objects.create(username="user1", is_active=True)
1692
1719
  ScheduledJob.objects.create(
1693
1720
  name="test1",
1694
- task="pass.TestPass",
1721
+ task="pass.TestPassJob",
1695
1722
  interval=JobExecutionType.TYPE_IMMEDIATELY,
1696
1723
  user=user,
1697
1724
  start_time=timezone.now(),
1698
1725
  )
1699
1726
  ScheduledJob.objects.create(
1700
1727
  name="test2",
1701
- task="pass.TestPass",
1728
+ task="pass.TestPassJob",
1702
1729
  interval=JobExecutionType.TYPE_DAILY,
1703
1730
  user=user,
1704
1731
  start_time=timezone.now(),
1705
1732
  )
1706
1733
  ScheduledJob.objects.create(
1707
1734
  name="test3",
1708
- task="pass.TestPass",
1735
+ task="pass.TestPassJob",
1709
1736
  interval=JobExecutionType.TYPE_CUSTOM,
1710
1737
  user=user,
1711
1738
  start_time=timezone.now(),
@@ -1719,7 +1746,7 @@ class ScheduledJobTestCase(
1719
1746
  ScheduledJob.objects.create(
1720
1747
  enabled=False,
1721
1748
  name="test4",
1722
- task="pass.TestPass",
1749
+ task="pass.TestPassJob",
1723
1750
  interval=JobExecutionType.TYPE_IMMEDIATELY,
1724
1751
  user=self.user,
1725
1752
  start_time=timezone.now(),
@@ -1736,7 +1763,7 @@ class ScheduledJobTestCase(
1736
1763
  ScheduledJob.objects.create(
1737
1764
  enabled=True,
1738
1765
  name=name,
1739
- task="pass.TestPass",
1766
+ task="pass.TestPassJob",
1740
1767
  interval=JobExecutionType.TYPE_CUSTOM,
1741
1768
  user=self.user,
1742
1769
  start_time=timezone.now(),
@@ -1767,7 +1794,7 @@ class ScheduledJobTestCase(
1767
1794
  ScheduledJob.objects.create(
1768
1795
  enabled=True,
1769
1796
  name="test11",
1770
- task="pass.TestPass",
1797
+ task="pass.TestPassJob",
1771
1798
  interval=JobExecutionType.TYPE_CUSTOM,
1772
1799
  user=self.user,
1773
1800
  start_time=timezone.now(),
@@ -1801,7 +1828,7 @@ class ApprovalQueueTestCase(
1801
1828
  def setUp(self):
1802
1829
  super().setUp()
1803
1830
  self.job_model = Job.objects.get_for_class_path("dry_run.TestDryRun")
1804
- self.job_model_2 = Job.objects.get_for_class_path("fail.TestFail")
1831
+ self.job_model_2 = Job.objects.get_for_class_path("fail.TestFailJob")
1805
1832
 
1806
1833
  ScheduledJob.objects.create(
1807
1834
  name="test1",
@@ -1814,7 +1841,7 @@ class ApprovalQueueTestCase(
1814
1841
  )
1815
1842
  ScheduledJob.objects.create(
1816
1843
  name="test2",
1817
- task="fail.TestFail",
1844
+ task="fail.TestFailJob",
1818
1845
  job_model=self.job_model_2,
1819
1846
  interval=JobExecutionType.TYPE_IMMEDIATELY,
1820
1847
  user=self.user,
@@ -1827,7 +1854,7 @@ class ApprovalQueueTestCase(
1827
1854
 
1828
1855
  ScheduledJob.objects.create(
1829
1856
  name="test4",
1830
- task="pass.TestPass",
1857
+ task="pass.TestPassJob",
1831
1858
  job_model=self.job_model,
1832
1859
  interval=JobExecutionType.TYPE_IMMEDIATELY,
1833
1860
  user=self.user,
@@ -2033,6 +2060,16 @@ class ApprovalQueueTestCase(
2033
2060
  self.assertEqual(1, len(ScheduledJob.objects.filter(pk=instance.pk)), msg=str(user))
2034
2061
 
2035
2062
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2063
+ @load_event_broker_override_settings(
2064
+ EVENT_BROKERS={
2065
+ "SyslogEventBroker": {
2066
+ "CLASS": "nautobot.core.events.SyslogEventBroker",
2067
+ "TOPICS": {
2068
+ "INCLUDE": ["*"],
2069
+ },
2070
+ }
2071
+ }
2072
+ )
2036
2073
  def test_post_deny_different_user_permitted(self):
2037
2074
  """A user with appropriate permissions can deny a job request."""
2038
2075
  user = User.objects.create_user(username="testuser1")
@@ -2053,10 +2090,19 @@ class ApprovalQueueTestCase(
2053
2090
  data = {"_deny": True}
2054
2091
 
2055
2092
  self.client.force_login(user)
2056
- response = self.client.post(self._get_url("view", instance), data)
2093
+ with self.assertLogs("nautobot.events") as cm:
2094
+ response = self.client.post(self._get_url("view", instance), data)
2057
2095
  self.assertRedirects(response, reverse("extras:scheduledjob_approval_queue_list"))
2058
2096
  # Request was deleted
2059
2097
  self.assertEqual(0, len(ScheduledJob.objects.filter(pk=instance.pk)))
2098
+ # Event was published
2099
+ expected_payload = {"data": serialize_object_v2(instance)}
2100
+ self.assertEqual(
2101
+ cm.output,
2102
+ [
2103
+ f"INFO:nautobot.events.nautobot.jobs.approval.denied:{json.dumps(expected_payload, cls=NautobotKombuJSONEncoder, indent=4)}"
2104
+ ],
2105
+ )
2060
2106
 
2061
2107
  # Check object-based permissions are enforced for a different instance
2062
2108
  instance = self._get_queryset().first()
@@ -2114,6 +2160,16 @@ class ApprovalQueueTestCase(
2114
2160
  self.assertIsNone(instance.approved_by_user)
2115
2161
 
2116
2162
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2163
+ @load_event_broker_override_settings(
2164
+ EVENT_BROKERS={
2165
+ "SyslogEventBroker": {
2166
+ "CLASS": "nautobot.core.events.SyslogEventBroker",
2167
+ "TOPICS": {
2168
+ "INCLUDE": ["*"],
2169
+ },
2170
+ }
2171
+ }
2172
+ )
2117
2173
  def test_post_approve_different_user_permitted(self):
2118
2174
  """A user with appropriate permissions can approve a job request."""
2119
2175
  user = User.objects.create_user(username="testuser1")
@@ -2134,11 +2190,21 @@ class ApprovalQueueTestCase(
2134
2190
  data = {"_approve": True}
2135
2191
 
2136
2192
  self.client.force_login(user)
2137
- response = self.client.post(self._get_url("view", instance), data)
2193
+ with self.assertLogs("nautobot.events") as cm:
2194
+ response = self.client.post(self._get_url("view", instance), data)
2195
+
2138
2196
  self.assertRedirects(response, reverse("extras:scheduledjob_approval_queue_list"))
2139
2197
  # Job was scheduled
2140
2198
  instance.refresh_from_db()
2141
2199
  self.assertEqual(instance.approved_by_user, user)
2200
+ # Event was published
2201
+ expected_payload = {"data": serialize_object_v2(instance)}
2202
+ self.assertEqual(
2203
+ cm.output,
2204
+ [
2205
+ f"INFO:nautobot.events.nautobot.jobs.approval.approved:{json.dumps(expected_payload, cls=NautobotKombuJSONEncoder, indent=4)}"
2206
+ ],
2207
+ )
2142
2208
 
2143
2209
  # Check object-based permissions are enforced for a different instance
2144
2210
  instance = self._get_queryset().last()
@@ -2149,6 +2215,26 @@ class ApprovalQueueTestCase(
2149
2215
  self.assertIsNone(instance.approved_by_user)
2150
2216
 
2151
2217
 
2218
+ class JobQueueTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2219
+ model = JobQueue
2220
+
2221
+ @classmethod
2222
+ def setUpTestData(cls):
2223
+ cls.form_data = {
2224
+ "name": "Test Job Queue",
2225
+ "queue_type": JobQueueTypeChoices.TYPE_CELERY,
2226
+ "description": "This is a very detailed description",
2227
+ "tenant": Tenant.objects.first().pk,
2228
+ "tags": [t.pk for t in Tag.objects.get_for_model(JobQueue)],
2229
+ }
2230
+ cls.bulk_edit_data = {
2231
+ "queue_type": JobQueueTypeChoices.TYPE_KUBERNETES,
2232
+ "description": "This is a very detailed new description",
2233
+ "tenant": Tenant.objects.last().pk,
2234
+ # TODO add tests for add_tags/remove_tags fields in TagsBulkEditFormMixin
2235
+ }
2236
+
2237
+
2152
2238
  class JobResultTestCase(
2153
2239
  ViewTestCases.GetObjectViewTestCase,
2154
2240
  ViewTestCases.ListObjectsViewTestCase,
@@ -2159,8 +2245,8 @@ class JobResultTestCase(
2159
2245
 
2160
2246
  @classmethod
2161
2247
  def setUpTestData(cls):
2162
- JobResult.objects.create(name="pass.TestPass")
2163
- JobResult.objects.create(name="fail.TestFail")
2248
+ JobResult.objects.create(name="pass.TestPassJob")
2249
+ JobResult.objects.create(name="fail.TestFailJob")
2164
2250
  JobLogEntry.objects.create(
2165
2251
  log_level=LogLevelChoices.LOG_INFO,
2166
2252
  job_result=JobResult.objects.first(),
@@ -2214,7 +2300,9 @@ class JobTestCase(
2214
2300
  # Job model objects are automatically created during database migrations
2215
2301
 
2216
2302
  # But we do need to make sure the ones we're testing are flagged appropriately
2217
- cls.test_pass = Job.objects.get(job_class_name="TestPass")
2303
+ cls.test_pass = Job.objects.get(job_class_name="TestPassJob")
2304
+ default_job_queue = JobQueue.objects.get(name="default", queue_type=JobQueueTypeChoices.TYPE_CELERY)
2305
+ cls.test_pass.default_job_queue = default_job_queue
2218
2306
  cls.test_pass.enabled = True
2219
2307
  cls.test_pass.save()
2220
2308
 
@@ -2227,6 +2315,7 @@ class JobTestCase(
2227
2315
 
2228
2316
  cls.test_required_args = Job.objects.get(job_class_name="TestRequired")
2229
2317
  cls.test_required_args.enabled = True
2318
+ cls.test_pass.default_job_queue = default_job_queue
2230
2319
  cls.test_required_args.save()
2231
2320
 
2232
2321
  cls.extra_run_urls = (
@@ -2245,12 +2334,16 @@ class JobTestCase(
2245
2334
  enabled=True,
2246
2335
  installed=False,
2247
2336
  )
2337
+ cls.test_not_installed.default_job_queue = default_job_queue
2248
2338
  cls.test_not_installed.validated_save()
2249
2339
 
2250
2340
  cls.data_run_immediately = {
2251
2341
  "_schedule_type": "immediately",
2252
2342
  }
2253
-
2343
+ job_queues = JobQueue.objects.all()[:3]
2344
+ pk_list = [queue.pk for queue in job_queues]
2345
+ pk_list += [default_job_queue.pk]
2346
+ job_queues = JobQueue.objects.filter(pk__in=pk_list)
2254
2347
  cls.form_data = {
2255
2348
  "enabled": True,
2256
2349
  "grouping_override": True,
@@ -2271,8 +2364,9 @@ class JobTestCase(
2271
2364
  "time_limit": 650,
2272
2365
  "has_sensitive_variables": False,
2273
2366
  "has_sensitive_variables_override": True,
2274
- "task_queues": "overridden,priority",
2275
- "task_queues_override": True,
2367
+ "job_queues": [queue.pk for queue in job_queues],
2368
+ "job_queues_override": True,
2369
+ "default_job_queue": default_job_queue.pk,
2276
2370
  }
2277
2371
  # This form is emulating the non-conventional JobBulkEditForm
2278
2372
  cls.bulk_edit_data = {
@@ -2293,8 +2387,10 @@ class JobTestCase(
2293
2387
  "time_limit": "",
2294
2388
  "has_sensitive_variables": False,
2295
2389
  "clear_has_sensitive_variables_override": False,
2296
- "task_queues": "overridden,priority",
2297
- "clear_task_queues_override": False,
2390
+ "job_queues": [queue.pk for queue in job_queues],
2391
+ "clear_job_queues_override": False,
2392
+ "clear_default_job_queue_override": False,
2393
+ "default_job_queue": default_job_queue.pk,
2298
2394
  }
2299
2395
 
2300
2396
  def get_deletable_object(self):
@@ -2340,35 +2436,6 @@ class JobTestCase(
2340
2436
  # assert Job still exists
2341
2437
  self.assertTrue(self._get_queryset().filter(name=job_name).exists())
2342
2438
 
2343
- def test_bulk_delete_system_jobs_fail(self):
2344
- system_job_queryset = self.model.objects.filter(module_name__startswith="nautobot.")
2345
- pk_list = system_job_queryset.values_list("pk", flat=True)[:3]
2346
- initial_count = self._get_queryset().count()
2347
- data = {
2348
- "pk": pk_list,
2349
- "confirm": True,
2350
- "_confirm": True, # Form button
2351
- }
2352
- # Try bulk delete with delete job permission
2353
- self.add_permissions("extras.delete_job")
2354
- response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2355
- self.assertBodyContains(
2356
- response,
2357
- f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted",
2358
- status_code=403,
2359
- )
2360
- self.assertEqual(self._get_queryset().count(), initial_count)
2361
-
2362
- # Try bulk delete as a superuser
2363
- self.user.is_superuser = True
2364
- response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2365
- self.assertBodyContains(
2366
- response,
2367
- f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted",
2368
- status_code=403,
2369
- )
2370
- self.assertEqual(self._get_queryset().count(), initial_count)
2371
-
2372
2439
  def validate_job_data_after_bulk_edit(self, pk_list, old_data):
2373
2440
  # Name is bulk-editable
2374
2441
  overridable_fields = [field for field in JOB_OVERRIDABLE_FIELDS if field != "name"]
@@ -2400,6 +2467,10 @@ class JobTestCase(
2400
2467
  else:
2401
2468
  self.assertEqual(getattr(instance, overridable_field), old_data[instance.pk][overridable_field])
2402
2469
  self.assertEqual(getattr(instance, override_field), old_data[instance.pk][overridable_field])
2470
+ # Special case for task queues/job queues
2471
+ override_value = self.bulk_edit_data.get("job_queues")
2472
+ self.assertEqual(list(instance.job_queues.values_list("pk", flat=True)), override_value)
2473
+ self.assertEqual(instance.job_queues_override, True)
2403
2474
 
2404
2475
  def validate_object_data_after_bulk_edit(self, pk_list):
2405
2476
  instances = self._get_queryset().filter(pk__in=pk_list)
@@ -2433,7 +2504,7 @@ class JobTestCase(
2433
2504
  self.add_permissions("extras.run_job")
2434
2505
  for run_url in self.run_urls:
2435
2506
  response = self.client.get(run_url)
2436
- self.assertBodyContains(response, "TestPass")
2507
+ self.assertBodyContains(response, "TestPassJob")
2437
2508
 
2438
2509
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
2439
2510
  def test_get_run_with_constrained_permission(self):
@@ -2532,12 +2603,12 @@ class JobTestCase(
2532
2603
  self.add_permissions("extras.run_job")
2533
2604
 
2534
2605
  for run_url in (
2535
- reverse("extras:job_run_by_class_path", kwargs={"class_path": "fail.TestFail"}),
2536
- reverse("extras:job_run", kwargs={"pk": Job.objects.get(job_class_name="TestFail").pk}),
2606
+ reverse("extras:job_run_by_class_path", kwargs={"class_path": "fail.TestFailJob"}),
2607
+ reverse("extras:job_run", kwargs={"pk": Job.objects.get(job_class_name="TestFailJob").pk}),
2537
2608
  ):
2538
2609
  response = self.client.post(run_url, self.data_run_immediately)
2539
2610
  self.assertBodyContains(response, "Job is not enabled to be run")
2540
- self.assertFalse(JobResult.objects.filter(name="fail.TestFail").exists())
2611
+ self.assertFalse(JobResult.objects.filter(name="fail.TestFailJob").exists())
2541
2612
 
2542
2613
  def test_run_now_missing_args(self):
2543
2614
  self.add_permissions("extras.run_job")
@@ -2565,19 +2636,24 @@ class JobTestCase(
2565
2636
  result = JobResult.objects.latest()
2566
2637
  self.assertRedirects(response, reverse("extras:jobresult", kwargs={"pk": result.pk}))
2567
2638
 
2568
- @mock.patch("nautobot.extras.jobs.task_queues_as_choices")
2569
- def test_rerun_job(self, mock_task_queues_as_choices):
2639
+ def test_rerun_job(self):
2570
2640
  self.add_permissions("extras.run_job")
2571
2641
  self.add_permissions("extras.view_jobresult")
2572
2642
 
2573
- mock_task_queues_as_choices.return_value = [("default", ""), ("queue1", ""), ("uniquequeue", "")]
2643
+ job_queue = JobQueue.objects.create(name="uniquequeue", queue_type=JobQueueTypeChoices.TYPE_CELERY)
2574
2644
  job_celery_kwargs = {
2575
2645
  "nautobot_job_job_model_id": self.test_required_args.id,
2576
2646
  "nautobot_job_profile": True,
2647
+ "nautobot_job_ignore_singleton_lock": True,
2577
2648
  "nautobot_job_user_id": self.user.id,
2578
- "queue": "uniquequeue",
2579
- }
2580
-
2649
+ "queue": job_queue.name,
2650
+ }
2651
+ self.test_required_args.job_queues.set([job_queue])
2652
+ self.test_required_args.is_singleton_override = True
2653
+ self.test_required_args.has_sensitive_variables_override = True
2654
+ self.test_required_args.is_singleton = True
2655
+ self.test_required_args.has_sensitive_variables = False
2656
+ self.test_required_args.validated_save()
2581
2657
  previous_result = JobResult.objects.create(
2582
2658
  job_model=self.test_required_args,
2583
2659
  user=self.user,
@@ -2588,13 +2664,15 @@ class JobTestCase(
2588
2664
  run_url = reverse("extras:job_run", kwargs={"pk": self.test_required_args.pk})
2589
2665
  response = self.client.get(f"{run_url}?kwargs_from_job_result={previous_result.pk!s}")
2590
2666
  content = extract_page_body(response.content.decode(response.charset))
2591
-
2592
- self.assertInHTML('<option value="uniquequeue" selected>', content)
2667
+ self.assertInHTML(f'<option value="{job_queue.pk}" selected>{job_queue}</option>', content)
2593
2668
  self.assertInHTML(
2594
2669
  '<input type="text" name="var" value="456" class="form-control" required placeholder="None" id="id_var">',
2595
2670
  content,
2596
2671
  )
2597
2672
  self.assertInHTML('<input type="hidden" name="_profile" value="True" id="id__profile">', content)
2673
+ self.assertInHTML(
2674
+ '<input type="checkbox" name="_ignore_singleton_lock" id="id__ignore_singleton_lock" checked>', content
2675
+ )
2598
2676
 
2599
2677
  @mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
2600
2678
  def test_run_later_missing_name(self, _):
@@ -2702,12 +2780,12 @@ class JobTestCase(
2702
2780
  self.add_permissions("extras.view_jobresult")
2703
2781
 
2704
2782
  self.test_pass.task_queues = []
2705
- self.test_pass.task_queues_override = True
2783
+ self.test_pass.job_queues_override = True
2706
2784
  self.test_pass.validated_save()
2707
-
2785
+ job_queue = JobQueue.objects.create(name="invalid", queue_type=JobQueueTypeChoices.TYPE_CELERY)
2708
2786
  data = {
2709
2787
  "_schedule_type": "immediately",
2710
- "_task_queue": "invalid",
2788
+ "_job_queue": job_queue.pk,
2711
2789
  }
2712
2790
 
2713
2791
  for run_url in self.run_urls:
@@ -2717,7 +2795,7 @@ class JobTestCase(
2717
2795
  errors = extract_form_failures(response.content.decode(response.charset))
2718
2796
  self.assertEqual(
2719
2797
  errors,
2720
- ["_task_queue: Select a valid choice. invalid is not one of the available choices."],
2798
+ ["_job_queue: Select a valid choice. That choice is not one of the available choices."],
2721
2799
  )
2722
2800
 
2723
2801
  @mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
@@ -2866,18 +2944,28 @@ class JobButtonRenderingTestCase(TestCase):
2866
2944
 
2867
2945
  def test_task_queue_hidden_input_is_present(self):
2868
2946
  """
2869
- Ensure that the job button respects the job class' task_queues and the job class task_queues[0]/default is passed as a hidden form input.
2947
+ Ensure that the job button respects the job class' task_queues and the job class default job queue is passed as a hidden form input.
2870
2948
  """
2871
- self.job.task_queues_override = True
2949
+ self.job.job_queues_override = True
2950
+ task_queues = ["overriden_queue", "default", "priority"]
2951
+ for queue in task_queues:
2952
+ JobQueue.objects.get_or_create(name=queue, defaults={"queue_type": JobQueueTypeChoices.TYPE_CELERY})
2872
2953
  self.job.task_queues = ["overriden_queue", "default", "priority"]
2873
2954
  self.job.save()
2874
2955
  response = self.client.get(self.location_type.get_absolute_url(), follow=True)
2875
- self.assertBodyContains(response, f'<input type="hidden" name="_task_queue" value="{self.job.task_queues[0]}">')
2876
- self.job.task_queues_override = False
2956
+ self.assertEqual(response.status_code, 200)
2957
+ content = extract_page_body(response.content.decode(response.charset))
2958
+ job_queues = self.job.job_queues.all().values_list("name", flat=True)
2959
+ self.assertIn(f'<input type="hidden" name="_job_queue" value="{job_queues[0]}">', content, content)
2960
+
2961
+ self.job.job_queues_override = False
2877
2962
  self.job.save()
2963
+ self.job.job_queues.set([])
2878
2964
  response = self.client.get(self.location_type.get_absolute_url(), follow=True)
2879
- self.assertBodyContains(
2880
- response, f'<input type="hidden" name="_task_queue" value="{settings.CELERY_TASK_DEFAULT_QUEUE}">'
2965
+ self.assertEqual(response.status_code, 200)
2966
+ content = extract_page_body(response.content.decode(response.charset))
2967
+ self.assertIn(
2968
+ f'<input type="hidden" name="_job_queue" value="{self.job.default_job_queue.name}">', content, content
2881
2969
  )
2882
2970
 
2883
2971
  def test_view_object_with_unsafe_text(self):
@@ -3138,93 +3226,6 @@ class RelationshipTestCase(
3138
3226
 
3139
3227
  cls.slug_test_object = "Primary Interface"
3140
3228
 
3141
- def test_required_relationships(self):
3142
- """
3143
- 1. Try creating an object when no required target object exists
3144
- 2. Try creating an object without specifying required target object(s)
3145
- 3. Try creating an object when all required data is present
3146
- 4. Test bulk edit
3147
- """
3148
-
3149
- # Delete existing factory generated objects that may interfere with this test
3150
- IPAddress.objects.all().delete()
3151
- Prefix.objects.update(parent=None)
3152
- Prefix.objects.all().delete()
3153
- VLAN.objects.all().delete()
3154
-
3155
- # Parameterized tests (for creating and updating single objects):
3156
- self.required_relationships_test(interact_with="ui")
3157
-
3158
- # 4. Bulk create/edit tests:
3159
-
3160
- vlan_status = Status.objects.get_for_model(VLAN).first()
3161
- vlans = (
3162
- VLAN.objects.create(name="test_required_relationships1", vid=1, status=vlan_status),
3163
- VLAN.objects.create(name="test_required_relationships2", vid=2, status=vlan_status),
3164
- VLAN.objects.create(name="test_required_relationships3", vid=3, status=vlan_status),
3165
- VLAN.objects.create(name="test_required_relationships4", vid=4, status=vlan_status),
3166
- VLAN.objects.create(name="test_required_relationships5", vid=5, status=vlan_status),
3167
- VLAN.objects.create(name="test_required_relationships6", vid=6, status=vlan_status),
3168
- )
3169
-
3170
- # Try deleting all devices and then editing the 6 VLANs (fails):
3171
- Controller.objects.filter(controller_device__isnull=False).delete()
3172
- Device.objects.all().delete()
3173
- response = self.client.post(
3174
- reverse("ipam:vlan_bulk_edit"), data={"pk": [str(vlan.id) for vlan in vlans], "_apply": [""]}
3175
- )
3176
- self.assertContains(response, "VLANs require at least one device, but no devices exist yet.")
3177
-
3178
- # Create test device for association
3179
- device_for_association = test_views.create_test_device("VLAN Required Device")
3180
-
3181
- # Try editing all 6 VLANs without adding the required device(fails):
3182
- response = self.client.post(
3183
- reverse("ipam:vlan_bulk_edit"), data={"pk": [str(vlan.id) for vlan in vlans], "_apply": [""]}
3184
- )
3185
- self.assertContains(
3186
- response,
3187
- "6 VLANs require a device for the required relationship &quot;VLANs require at least one Device&quot;",
3188
- )
3189
-
3190
- # Try editing 3 VLANs without adding the required device(fails):
3191
- response = self.client.post(
3192
- reverse("ipam:vlan_bulk_edit"), data={"pk": [str(vlan.id) for vlan in vlans[:3]], "_apply": [""]}
3193
- )
3194
- self.assertContains(
3195
- response,
3196
- "These VLANs require a device for the required "
3197
- "relationship &quot;VLANs require at least one Device&quot;",
3198
- )
3199
- for vlan in vlans[:3]:
3200
- self.assertContains(response, str(vlan))
3201
-
3202
- # Try editing 6 VLANs and adding the required device (succeeds):
3203
- response = self.client.post(
3204
- reverse("ipam:vlan_bulk_edit"),
3205
- data={
3206
- "pk": [str(vlan.id) for vlan in vlans],
3207
- "add_cr_vlans_devices_m2m__source": [str(device_for_association.id)],
3208
- "_apply": [""],
3209
- },
3210
- follow=True,
3211
- )
3212
- self.assertContains(response, "Updated 6 VLANs")
3213
-
3214
- # Try editing 6 VLANs and removing the required device (fails):
3215
- response = self.client.post(
3216
- reverse("ipam:vlan_bulk_edit"),
3217
- data={
3218
- "pk": [str(vlan.id) for vlan in vlans],
3219
- "remove_cr_vlans_devices_m2m__source": [str(device_for_association.id)],
3220
- "_apply": [""],
3221
- },
3222
- )
3223
- self.assertContains(
3224
- response,
3225
- "6 VLANs require a device for the required relationship &quot;VLANs require at least one Device&quot;",
3226
- )
3227
-
3228
3229
 
3229
3230
  class RelationshipAssociationTestCase(
3230
3231
  # TODO? ViewTestCases.CreateObjectViewTestCase,
@@ -345,7 +345,8 @@ class WebhookTest(APITestCase):
345
345
 
346
346
  all_changes = get_changes_for_model(location)
347
347
  self.assertEqual(all_changes.count(), 1)
348
- mock_enqueue_webhooks.assert_called_once_with(all_changes.first(), webhook_queryset=None)
348
+ change = all_changes.first()
349
+ mock_enqueue_webhooks.assert_called_once_with(change, snapshots=change.get_snapshots(), webhook_queryset=None)
349
350
 
350
351
  def test_all_webhook_supported_models(self):
351
352
  """
nautobot/extras/urls.py CHANGED
@@ -16,7 +16,6 @@ from nautobot.extras.models import (
16
16
  JobHook,
17
17
  Note,
18
18
  Relationship,
19
- Secret,
20
19
  SecretsGroup,
21
20
  Status,
22
21
  Tag,
@@ -30,10 +29,12 @@ router.register("contacts", views.ContactUIViewSet)
30
29
  router.register("contact-associations", views.ContactAssociationUIViewSet)
31
30
  router.register("external-integrations", views.ExternalIntegrationUIViewSet)
32
31
  router.register("job-buttons", views.JobButtonUIViewSet)
32
+ router.register("job-queues", views.JobQueueUIViewSet)
33
33
  router.register("metadata-types", views.MetadataTypeUIViewSet)
34
34
  router.register("object-metadata", views.ObjectMetadataUIViewSet)
35
35
  router.register("roles", views.RoleUIViewSet)
36
36
  router.register("saved-views", views.SavedViewUIViewSet)
37
+ router.register("secrets", views.SecretUIViewSet)
37
38
  router.register("static-group-associations", views.StaticGroupAssociationUIViewSet)
38
39
  router.register("teams", views.TeamUIViewSet)
39
40
 
@@ -565,30 +566,11 @@ urlpatterns = [
565
566
  name="relationshipassociation_delete",
566
567
  ),
567
568
  # Secrets
568
- path("secrets/", views.SecretListView.as_view(), name="secret_list"),
569
- path("secrets/add/", views.SecretEditView.as_view(), name="secret_add"),
570
- path("secrets/delete/", views.SecretBulkDeleteView.as_view(), name="secret_bulk_delete"),
571
- path("secrets/import/", views.SecretBulkImportView.as_view(), name="secret_import"), # 3.0 TODO: remove, unused
572
569
  path(
573
570
  "secrets/provider/<str:provider_slug>/form/",
574
571
  views.SecretProviderParametersFormView.as_view(),
575
572
  name="secret_provider_parameters_form",
576
573
  ),
577
- path("secrets/<uuid:pk>/", views.SecretView.as_view(), name="secret"),
578
- path("secrets/<uuid:pk>/edit/", views.SecretEditView.as_view(), name="secret_edit"),
579
- path("secrets/<uuid:pk>/delete/", views.SecretDeleteView.as_view(), name="secret_delete"),
580
- path(
581
- "secrets/<uuid:pk>/changelog/",
582
- views.ObjectChangeLogView.as_view(),
583
- name="secret_changelog",
584
- kwargs={"model": Secret},
585
- ),
586
- path(
587
- "secrets/<uuid:pk>/notes/",
588
- views.ObjectNotesView.as_view(),
589
- name="secret_notes",
590
- kwargs={"model": Secret},
591
- ),
592
574
  path("secrets-groups/", views.SecretsGroupListView.as_view(), name="secretsgroup_list"),
593
575
  path("secrets-groups/add/", views.SecretsGroupEditView.as_view(), name="secretsgroup_add"),
594
576
  path("secrets-groups/delete/", views.SecretsGroupBulkDeleteView.as_view(), name="secretsgroup_bulk_delete"),