nautobot 2.2.2__py3-none-any.whl → 2.2.4__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 (362) hide show
  1. nautobot/apps/jobs.py +2 -0
  2. nautobot/core/api/utils.py +12 -9
  3. nautobot/core/apps/__init__.py +2 -2
  4. nautobot/core/celery/__init__.py +79 -68
  5. nautobot/core/celery/backends.py +9 -1
  6. nautobot/core/celery/control.py +4 -7
  7. nautobot/core/celery/schedulers.py +4 -2
  8. nautobot/core/celery/task.py +78 -5
  9. nautobot/core/graphql/schema.py +2 -1
  10. nautobot/core/jobs/__init__.py +2 -1
  11. nautobot/core/settings.py +6 -4
  12. nautobot/core/settings.yaml +51 -16
  13. nautobot/core/templates/admin/base.html +2 -2
  14. nautobot/core/templates/base_django.html +2 -2
  15. nautobot/core/templates/buttons/export.html +47 -47
  16. nautobot/core/templates/generic/object_list.html +3 -3
  17. nautobot/core/templates/inc/javascript.html +3 -0
  18. nautobot/core/templates/inc/media.html +3 -0
  19. nautobot/core/templates/login.html +2 -2
  20. nautobot/core/templates/nautobot_config.py.j2 +2 -0
  21. nautobot/core/templatetags/helpers.py +66 -9
  22. nautobot/core/testing/__init__.py +6 -1
  23. nautobot/core/testing/api.py +12 -13
  24. nautobot/core/testing/mixins.py +2 -2
  25. nautobot/core/testing/views.py +50 -51
  26. nautobot/core/tests/test_api.py +23 -2
  27. nautobot/core/tests/test_jobs.py +79 -2
  28. nautobot/core/tests/test_templatetags_helpers.py +32 -0
  29. nautobot/core/tests/test_views.py +52 -0
  30. nautobot/core/tests/test_views_utils.py +22 -1
  31. nautobot/core/utils/module_loading.py +89 -0
  32. nautobot/core/views/mixins.py +4 -0
  33. nautobot/core/views/utils.py +3 -2
  34. nautobot/dcim/choices.py +14 -0
  35. nautobot/dcim/forms.py +51 -1
  36. nautobot/dcim/models/device_components.py +9 -5
  37. nautobot/dcim/templates/dcim/location.html +32 -13
  38. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +102 -0
  39. nautobot/dcim/tests/test_views.py +376 -55
  40. nautobot/dcim/urls.py +5 -0
  41. nautobot/dcim/views.py +172 -21
  42. nautobot/extras/api/serializers.py +17 -6
  43. nautobot/extras/api/views.py +21 -10
  44. nautobot/extras/constants.py +3 -3
  45. nautobot/extras/datasources/git.py +47 -58
  46. nautobot/extras/forms/forms.py +3 -1
  47. nautobot/extras/jobs.py +79 -146
  48. nautobot/extras/models/datasources.py +0 -2
  49. nautobot/extras/models/jobs.py +36 -18
  50. nautobot/extras/plugins/__init__.py +1 -20
  51. nautobot/extras/signals.py +6 -9
  52. nautobot/extras/test_jobs/__init__.py +8 -0
  53. nautobot/extras/test_jobs/dry_run.py +3 -2
  54. nautobot/extras/test_jobs/fail.py +43 -0
  55. nautobot/extras/test_jobs/ipaddress_vars.py +40 -1
  56. nautobot/extras/test_jobs/jobs_module/__init__.py +5 -0
  57. nautobot/extras/test_jobs/jobs_module/jobs_submodule/__init__.py +1 -0
  58. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +6 -0
  59. nautobot/extras/test_jobs/pass.py +40 -0
  60. nautobot/extras/test_jobs/relative_import.py +11 -0
  61. nautobot/extras/tests/test_api.py +3 -0
  62. nautobot/extras/tests/test_context_managers.py +18 -0
  63. nautobot/extras/tests/test_datasources.py +125 -118
  64. nautobot/extras/tests/test_job_variables.py +57 -15
  65. nautobot/extras/tests/test_jobs.py +135 -1
  66. nautobot/extras/tests/test_models.py +26 -19
  67. nautobot/extras/tests/test_plugins.py +1 -3
  68. nautobot/extras/tests/test_views.py +2 -4
  69. nautobot/extras/utils.py +2 -1
  70. nautobot/extras/views.py +82 -116
  71. nautobot/ipam/api/views.py +8 -1
  72. nautobot/ipam/graphql/types.py +11 -0
  73. nautobot/ipam/mixins.py +32 -0
  74. nautobot/ipam/models.py +2 -1
  75. nautobot/ipam/querysets.py +6 -1
  76. nautobot/ipam/tests/test_models.py +82 -0
  77. nautobot/ipam/views.py +6 -6
  78. nautobot/project-static/docs/404.html +107 -51
  79. nautobot/project-static/docs/apps/index.html +107 -51
  80. nautobot/project-static/docs/apps/nautobot-apps.html +107 -51
  81. nautobot/project-static/docs/assets/_mkdocstrings.css +6 -1
  82. nautobot/project-static/docs/assets/extra.css +11 -0
  83. nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js +29 -0
  84. nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js.map +7 -0
  85. nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css +1 -0
  86. nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css.map +1 -0
  87. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +107 -51
  88. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +107 -51
  89. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +108 -52
  90. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +107 -51
  91. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +107 -51
  92. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +107 -51
  93. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +107 -51
  94. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +107 -51
  95. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +107 -51
  96. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +107 -51
  97. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +107 -51
  98. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +107 -51
  99. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +107 -51
  100. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +287 -262
  101. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +107 -51
  102. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +107 -51
  103. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +107 -51
  104. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +107 -51
  105. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +107 -51
  106. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +107 -51
  107. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +107 -51
  108. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +107 -51
  109. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +107 -51
  110. nautobot/project-static/docs/development/apps/api/configuration-view.html +110 -54
  111. nautobot/project-static/docs/development/apps/api/database-backend-config.html +110 -54
  112. nautobot/project-static/docs/development/apps/api/models/django-admin.html +107 -51
  113. nautobot/project-static/docs/development/apps/api/models/global-search.html +110 -54
  114. nautobot/project-static/docs/development/apps/api/models/graphql.html +113 -57
  115. nautobot/project-static/docs/development/apps/api/models/index.html +107 -51
  116. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +113 -57
  117. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +107 -51
  118. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +110 -54
  119. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +107 -51
  120. nautobot/project-static/docs/development/apps/api/platform-features/index.html +107 -51
  121. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +110 -54
  122. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +111 -55
  123. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +110 -54
  124. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +110 -54
  125. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +107 -51
  126. nautobot/project-static/docs/development/apps/api/prometheus.html +110 -54
  127. nautobot/project-static/docs/development/apps/api/setup.html +107 -51
  128. nautobot/project-static/docs/development/apps/api/testing.html +113 -57
  129. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +110 -54
  130. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +110 -54
  131. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +107 -51
  132. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +107 -51
  133. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +113 -57
  134. nautobot/project-static/docs/development/apps/api/views/base-template.html +107 -51
  135. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +110 -54
  136. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +107 -51
  137. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +110 -54
  138. nautobot/project-static/docs/development/apps/api/views/index.html +107 -51
  139. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +113 -57
  140. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +122 -66
  141. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +110 -54
  142. nautobot/project-static/docs/development/apps/api/views/notes.html +110 -54
  143. nautobot/project-static/docs/development/apps/api/views/rest-api.html +107 -51
  144. nautobot/project-static/docs/development/apps/api/views/urls.html +107 -51
  145. nautobot/project-static/docs/development/apps/index.html +128 -72
  146. nautobot/project-static/docs/development/apps/migration/code-updates.html +107 -51
  147. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +107 -51
  148. nautobot/project-static/docs/development/apps/migration/from-v1.html +107 -51
  149. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +107 -51
  150. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +107 -51
  151. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +107 -51
  152. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +107 -51
  153. nautobot/project-static/docs/development/apps/porting-from-netbox.html +110 -54
  154. nautobot/project-static/docs/development/core/application-registry.html +242 -144
  155. nautobot/project-static/docs/development/core/best-practices.html +122 -66
  156. nautobot/project-static/docs/development/core/bootstrap-ui.html +107 -51
  157. nautobot/project-static/docs/development/core/caching.html +107 -51
  158. nautobot/project-static/docs/development/core/controllers.html +107 -51
  159. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +113 -57
  160. nautobot/project-static/docs/development/core/generic-views.html +110 -54
  161. nautobot/project-static/docs/development/core/getting-started.html +135 -79
  162. nautobot/project-static/docs/development/core/homepage.html +110 -54
  163. nautobot/project-static/docs/development/core/index.html +107 -51
  164. nautobot/project-static/docs/development/core/model-checklist.html +156 -52
  165. nautobot/project-static/docs/development/core/model-features.html +108 -52
  166. nautobot/project-static/docs/development/core/natural-keys.html +110 -54
  167. nautobot/project-static/docs/development/core/navigation-menu.html +107 -51
  168. nautobot/project-static/docs/development/core/release-checklist.html +107 -51
  169. nautobot/project-static/docs/development/core/role-internals.html +107 -51
  170. nautobot/project-static/docs/development/core/settings.html +107 -51
  171. nautobot/project-static/docs/development/core/style-guide.html +110 -54
  172. nautobot/project-static/docs/development/core/templates.html +113 -57
  173. nautobot/project-static/docs/development/core/testing.html +125 -69
  174. nautobot/project-static/docs/development/core/user-preferences.html +107 -51
  175. nautobot/project-static/docs/development/index.html +107 -51
  176. nautobot/project-static/docs/development/jobs/index.html +504 -172
  177. nautobot/project-static/docs/development/jobs/migration/from-v1.html +111 -55
  178. nautobot/project-static/docs/docker/index.html +3 -3
  179. nautobot/project-static/docs/index.html +125 -69
  180. nautobot/project-static/docs/installation/selinux-troubleshooting.html +3 -3
  181. nautobot/project-static/docs/objects.inv +0 -0
  182. nautobot/project-static/docs/release-notes/index.html +107 -51
  183. nautobot/project-static/docs/release-notes/version-1.0.html +107 -51
  184. nautobot/project-static/docs/release-notes/version-1.1.html +107 -51
  185. nautobot/project-static/docs/release-notes/version-1.2.html +107 -51
  186. nautobot/project-static/docs/release-notes/version-1.3.html +107 -51
  187. nautobot/project-static/docs/release-notes/version-1.4.html +107 -51
  188. nautobot/project-static/docs/release-notes/version-1.5.html +116 -60
  189. nautobot/project-static/docs/release-notes/version-1.6.html +107 -51
  190. nautobot/project-static/docs/release-notes/version-2.0.html +110 -54
  191. nautobot/project-static/docs/release-notes/version-2.1.html +107 -51
  192. nautobot/project-static/docs/release-notes/version-2.2.html +500 -114
  193. nautobot/project-static/docs/requirements.txt +2 -2
  194. nautobot/project-static/docs/search/search_index.json +1 -1
  195. nautobot/project-static/docs/sitemap.xml +262 -262
  196. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  197. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +107 -51
  198. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +107 -51
  199. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +107 -51
  200. nautobot/project-static/docs/user-guide/administration/configuration/index.html +107 -51
  201. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +251 -164
  202. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +113 -57
  203. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +107 -51
  204. nautobot/project-static/docs/user-guide/administration/guides/caching.html +113 -57
  205. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +107 -51
  206. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +107 -51
  207. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +107 -51
  208. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +113 -57
  209. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +107 -51
  210. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +107 -51
  211. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +107 -51
  212. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +171 -112
  213. nautobot/project-static/docs/user-guide/administration/installation/docker.html +13 -8626
  214. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +117 -61
  215. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +13 -8614
  216. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +252 -165
  217. nautobot/project-static/docs/user-guide/administration/installation/index.html +165 -192
  218. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +411 -691
  219. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +248 -229
  220. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +13 -8118
  221. nautobot/project-static/docs/user-guide/administration/installation/services.html +350 -240
  222. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +8684 -0
  223. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +8672 -0
  224. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +8176 -0
  225. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +110 -54
  226. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +110 -54
  227. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +155 -99
  228. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +107 -51
  229. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +109 -53
  230. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +107 -51
  231. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +107 -51
  232. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +107 -51
  233. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +107 -51
  234. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +107 -51
  235. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +107 -51
  236. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +117 -58
  237. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +113 -57
  238. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +107 -51
  239. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +107 -51
  240. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +107 -51
  241. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +107 -51
  242. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +110 -54
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +107 -51
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +110 -54
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +110 -54
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +110 -54
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +110 -54
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +107 -51
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +107 -51
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +113 -57
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +110 -54
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +110 -54
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +110 -54
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +110 -54
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +116 -60
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +110 -54
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +110 -54
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +119 -63
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +110 -54
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +110 -54
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +113 -57
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +113 -57
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +113 -57
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +107 -51
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +113 -57
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +107 -51
  267. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +110 -54
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +110 -54
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +107 -51
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +110 -54
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +110 -54
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +107 -51
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +107 -51
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +107 -51
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +110 -54
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +110 -54
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +110 -54
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +110 -54
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +107 -51
  280. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +113 -57
  281. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +110 -54
  282. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +110 -54
  283. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +110 -54
  284. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +125 -69
  285. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +113 -57
  286. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +128 -72
  287. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +110 -54
  288. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +107 -51
  289. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +110 -54
  290. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +227 -60
  291. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +107 -51
  292. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +113 -57
  293. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +107 -51
  294. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +110 -54
  295. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +107 -51
  296. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +107 -51
  297. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +107 -51
  298. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +107 -51
  299. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +113 -57
  300. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +113 -57
  301. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +107 -51
  302. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +113 -57
  303. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +107 -51
  304. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +107 -51
  305. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +107 -51
  306. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +107 -51
  307. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +107 -51
  308. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +107 -51
  309. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +107 -51
  310. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +107 -51
  311. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +107 -51
  312. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +110 -54
  313. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +113 -57
  314. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +107 -51
  315. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +110 -54
  316. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +107 -51
  317. nautobot/project-static/docs/user-guide/index.html +109 -53
  318. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +107 -51
  319. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +113 -57
  320. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +128 -72
  321. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +107 -51
  322. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +125 -69
  323. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +107 -51
  324. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +110 -54
  325. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +125 -69
  326. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +110 -54
  327. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +107 -51
  328. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +107 -51
  329. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +143 -100
  330. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +113 -57
  331. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +113 -57
  332. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +110 -54
  333. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +123 -67
  334. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +107 -51
  335. nautobot/project-static/docs/user-guide/platform-functionality/note.html +110 -54
  336. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +116 -60
  337. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +110 -54
  338. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +131 -75
  339. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +149 -93
  340. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +110 -54
  341. nautobot/project-static/docs/user-guide/platform-functionality/role.html +107 -51
  342. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +116 -60
  343. nautobot/project-static/docs/user-guide/platform-functionality/status.html +119 -63
  344. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +110 -54
  345. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +137 -81
  346. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +110 -54
  347. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +107 -51
  348. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +110 -54
  349. nautobot/project-static/js/forms.js +18 -11
  350. nautobot/tenancy/views.py +2 -6
  351. nautobot/virtualization/views.py +5 -9
  352. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/METADATA +4 -4
  353. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/RECORD +357 -348
  354. nautobot/extras/test_jobs/job_variables.py +0 -93
  355. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js +0 -29
  356. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js.map +0 -7
  357. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +0 -1
  358. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +0 -1
  359. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/LICENSE.txt +0 -0
  360. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/NOTICE +0 -0
  361. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/WHEEL +0 -0
  362. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/entry_points.txt +0 -0
@@ -31,7 +31,6 @@ logger = getLogger(__name__)
31
31
  registry["plugin_banners"] = []
32
32
  registry["plugin_custom_validators"] = collections.defaultdict(list)
33
33
  registry["plugin_graphql_types"] = []
34
- registry["plugin_jobs"] = []
35
34
  registry["plugin_template_extensions"] = collections.defaultdict(list)
36
35
  registry["app_metrics"] = []
37
36
 
@@ -141,9 +140,9 @@ class NautobotAppConfig(NautobotConfig):
141
140
  register_graphql_types(graphql_types)
142
141
 
143
142
  # Import jobs (if present)
143
+ # Note that we do *not* auto-call `register_jobs()` - the App is responsible for doing so when imported.
144
144
  jobs = import_object(f"{self.__module__}.{self.jobs}")
145
145
  if jobs is not None:
146
- register_jobs(jobs)
147
146
  self.features["jobs"] = jobs
148
147
 
149
148
  # Import metrics (if present)
@@ -423,24 +422,6 @@ def register_graphql_types(class_list):
423
422
  registry["plugin_graphql_types"].append(item)
424
423
 
425
424
 
426
- def register_jobs(class_list):
427
- """
428
- Register a list of Job classes
429
- """
430
- from nautobot.extras.jobs import Job
431
-
432
- for job in class_list:
433
- if not inspect.isclass(job):
434
- raise TypeError(f"Job class {job} was passed as an instance!")
435
- if not issubclass(job, Job):
436
- raise TypeError(f"{job} is not a subclass of extras.jobs.Job!")
437
-
438
- registry["plugin_jobs"].append(job)
439
-
440
- # Note that we do not (and cannot) update the Job records in the Nautobot database at this time.
441
- # That is done in response to the `nautobot_database_ready` signal, see nautobot.extras.signals.refresh_job_models
442
-
443
-
444
425
  def register_metrics(function_list):
445
426
  """
446
427
  Register a list of metric functions
@@ -21,7 +21,7 @@ from django.utils import timezone
21
21
  from django_prometheus.models import model_deletes, model_inserts, model_updates
22
22
  import redis.exceptions
23
23
 
24
- from nautobot.core.celery import app, import_jobs_as_celery_tasks
24
+ from nautobot.core.celery import app, import_jobs
25
25
  from nautobot.core.models import BaseModel
26
26
  from nautobot.core.utils.config import get_settings_or_config
27
27
  from nautobot.core.utils.logging import sanitize
@@ -333,7 +333,7 @@ def git_repository_pre_delete(instance, **kwargs):
333
333
  app.control.broadcast("discard_git_repository", repository_slug=instance.slug)
334
334
  # But we don't have an equivalent way to broadcast to any other Django instances.
335
335
  # For now we just delete the one that we have locally and rely on other methods,
336
- # such as the import_jobs_as_celery_tasks() signal that runs on server startup,
336
+ # such as the import_jobs() signal that runs on server startup,
337
337
  # to clean up other clones as they're encountered.
338
338
  if os.path.isdir(instance.filesystem_path):
339
339
  shutil.rmtree(instance.filesystem_path)
@@ -462,7 +462,7 @@ def refresh_job_models(sender, *, apps, **kwargs):
462
462
  """
463
463
  Callback for the nautobot_database_ready signal; updates Jobs in the database based on Job source file availability.
464
464
  """
465
- from nautobot.extras.jobs import Job as JobClass # avoid circular import
465
+ from nautobot.extras.jobs import get_jobs # avoid circular import
466
466
 
467
467
  Job = apps.get_model("extras", "Job")
468
468
 
@@ -471,15 +471,12 @@ def refresh_job_models(sender, *, apps, **kwargs):
471
471
  logger.info("Skipping refresh_job_models() as it appears Job model has not yet been migrated to latest.")
472
472
  return
473
473
 
474
- import_jobs_as_celery_tasks(app)
474
+ import_jobs()
475
475
 
476
476
  job_models = []
477
- for task in app.tasks.values():
478
- # Skip Celery tasks that aren't Jobs
479
- if not isinstance(task, JobClass):
480
- continue
481
477
 
482
- job_model, _ = refresh_job_model_from_job_class(Job, task.__class__)
478
+ for job_class in get_jobs().values():
479
+ job_model, _ = refresh_job_model_from_job_class(Job, job_class)
483
480
  if job_model is not None:
484
481
  job_models.append(job_model)
485
482
 
@@ -0,0 +1,8 @@
1
+ def load_tests(*args):
2
+ """Implement unittest discovery for this submodule as a no-op.
3
+
4
+ This prevents unittest from recursively loading all of the modules under this directory to inspect whether they
5
+ define test cases. This is necessary because otherwise the `jobs_module` submodule will get loaded when tests run,
6
+ which will in turn call `register_jobs()`, incorrectly/unexpectedly registering the test Job defined in that module
7
+ as if it were a system Job, which will cause tests to fail due to the unexpected presence of this Job.
8
+ """
@@ -1,5 +1,5 @@
1
1
  from nautobot.core.celery import register_jobs
2
- from nautobot.extras.jobs import DryRunVar, get_task_logger, Job
2
+ from nautobot.extras.jobs import DryRunVar, get_task_logger, IntegerVar, Job
3
3
  from nautobot.extras.models import Status
4
4
 
5
5
  logger = get_task_logger(__name__)
@@ -11,8 +11,9 @@ class TestDryRun(Job):
11
11
  """
12
12
 
13
13
  dryrun = DryRunVar()
14
+ value = IntegerVar(required=False)
14
15
 
15
- def run(self, dryrun):
16
+ def run(self, dryrun, value=None):
16
17
  """
17
18
  Job function.
18
19
  """
@@ -1,4 +1,7 @@
1
+ from billiard.einfo import ExceptionInfo
2
+
1
3
  from nautobot.core.celery import register_jobs
4
+ from nautobot.extras.choices import JobResultStatusChoices
2
5
  from nautobot.extras.jobs import get_task_logger, Job, RunJobTaskFailed
3
6
 
4
7
  logger = get_task_logger(__name__)
@@ -11,6 +14,15 @@ class TestFail(Job):
11
14
 
12
15
  description = "Validate job import"
13
16
 
17
+ def before_start(self, task_id, args, kwargs):
18
+ if task_id != self.request.id:
19
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}")
20
+ if args:
21
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
22
+ if kwargs:
23
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
24
+ logger.info("before_start() was called as expected")
25
+
14
26
  def run(self):
15
27
  """
16
28
  Job function.
@@ -18,6 +30,37 @@ class TestFail(Job):
18
30
  logger.info("I'm a test job that fails!")
19
31
  raise RunJobTaskFailed("Test failure")
20
32
 
33
+ def on_success(self, retval, task_id, args, kwargs):
34
+ raise RuntimeError("on_success() was unexpectedly called!")
35
+
36
+ def on_failure(self, exc, task_id, args, kwargs, einfo):
37
+ if not isinstance(exc, RunJobTaskFailed):
38
+ raise RuntimeError(f"Expected exc to be a RunJobTaskFailed, but it was {exc!r}")
39
+ if task_id != self.request.id:
40
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}")
41
+ if args:
42
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
43
+ if kwargs:
44
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
45
+ if not isinstance(einfo, ExceptionInfo):
46
+ raise RuntimeError(f"Expected einfo to be an ExceptionInfo, but it was {einfo!r}")
47
+ logger.info("on_failure() was called as expected")
48
+
49
+ def after_return(self, status, retval, task_id, args, kwargs, einfo):
50
+ if status is not JobResultStatusChoices.STATUS_FAILURE:
51
+ raise RuntimeError(f"Expected status to be {JobResultStatusChoices.STATUS_FAILURE}, but it was {status!r}")
52
+ if not isinstance(retval, RunJobTaskFailed):
53
+ raise RuntimeError(f"Expected retval to be a RunJobTaskFailed, but it was {retval!r}")
54
+ if task_id != self.request.id:
55
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}")
56
+ if args:
57
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
58
+ if kwargs:
59
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
60
+ if not isinstance(einfo, ExceptionInfo):
61
+ raise RuntimeError(f"Expected einfo to be an ExceptionInfo, but it was {einfo!r}")
62
+ logger.info("after_return() was called as expected")
63
+
21
64
 
22
65
  class TestFailWithSanitization(Job):
23
66
  """
@@ -1,5 +1,7 @@
1
1
  import json
2
2
 
3
+ import netaddr
4
+
3
5
  from nautobot.core.celery import register_jobs
4
6
  from nautobot.extras.jobs import get_task_logger, IPAddressVar, IPAddressWithMaskVar, Job
5
7
 
@@ -30,7 +32,26 @@ class TestIPAddresses(Job):
30
32
  description="IPv6 network",
31
33
  )
32
34
 
33
- def run(self, ipv4_address, ipv4_with_mask, ipv4_network, ipv6_address, ipv6_with_mask, ipv6_network):
35
+ def before_start(self, task_id, args, kwargs):
36
+ for expected_kwarg in self._get_vars().keys():
37
+ if expected_kwarg not in kwargs:
38
+ raise RuntimeError(f"kwargs should contain {expected_kwarg} but it doesn't!")
39
+ if kwargs[expected_kwarg] is None:
40
+ raise RuntimeError(f"kwargs[{expected_kwarg}] is unexpectedly None!")
41
+
42
+ def run(self, *, ipv4_address, ipv4_with_mask, ipv4_network, ipv6_address, ipv6_with_mask, ipv6_network):
43
+ if not isinstance(ipv4_address, netaddr.IPAddress):
44
+ raise RuntimeError(f"Expected ipv4_address to be a netaddr.IPAddress, but it was {ipv4_address!r}")
45
+ if not isinstance(ipv4_with_mask, netaddr.IPNetwork):
46
+ raise RuntimeError(f"Expected ipv4_with_mask to be a netaddr.IPNetwork, but it was {ipv4_with_mask!r}")
47
+ if not isinstance(ipv4_network, netaddr.IPNetwork):
48
+ raise RuntimeError(f"Expected ipv4_network to be a netaddr.IPNetwork, but it was {ipv4_network!r}")
49
+ if not isinstance(ipv6_address, netaddr.IPAddress):
50
+ raise RuntimeError(f"Expected ipv6_address to be a netaddr.IPAddress, but it was {ipv6_address!r}")
51
+ if not isinstance(ipv6_with_mask, netaddr.IPNetwork):
52
+ raise RuntimeError(f"Expected ipv6_with_mask to be a netaddr.IPNetwork, but it was {ipv6_with_mask!r}")
53
+ if not isinstance(ipv6_network, netaddr.IPNetwork):
54
+ raise RuntimeError(f"Expected ipv6_network to be a netaddr.IPNetwork, but it was {ipv6_network!r}")
34
55
  # Log the data as JSON so we can pull it back out for testing.
35
56
  logger.info(
36
57
  "IP Address Test",
@@ -59,5 +80,23 @@ class TestIPAddresses(Job):
59
80
 
60
81
  return "Nice IPs, bro."
61
82
 
83
+ def on_success(self, retval, task_id, args, kwargs):
84
+ if retval != "Nice IPs, bro.":
85
+ raise RuntimeError(f"retval is unexpected: {retval!r}")
86
+ for expected_kwarg in self._get_vars().keys():
87
+ if expected_kwarg not in kwargs:
88
+ raise RuntimeError(f"kwargs should contain {expected_kwarg} but it doesn't!")
89
+ if kwargs[expected_kwarg] is None:
90
+ raise RuntimeError(f"kwargs[{expected_kwarg}] is unexpectedly None!")
91
+
92
+ def after_return(self, status, retval, task_id, args, kwargs, einfo):
93
+ if retval != "Nice IPs, bro.":
94
+ raise RuntimeError(f"retval is unexpected: {retval!r}")
95
+ for expected_kwarg in self._get_vars().keys():
96
+ if expected_kwarg not in kwargs:
97
+ raise RuntimeError(f"kwargs should contain {expected_kwarg} but it doesn't!")
98
+ if kwargs[expected_kwarg] is None:
99
+ raise RuntimeError(f"kwargs[{expected_kwarg}] is unexpectedly None!")
100
+
62
101
 
63
102
  register_jobs(TestIPAddresses)
@@ -0,0 +1,5 @@
1
+ from nautobot.apps.jobs import register_jobs
2
+
3
+ from .jobs_submodule import ChildJob
4
+
5
+ register_jobs(ChildJob)
@@ -0,0 +1 @@
1
+ from .jobs import ChildJob # noqa: F401
@@ -0,0 +1,6 @@
1
+ from nautobot.apps.jobs import Job
2
+
3
+
4
+ class ChildJob(Job):
5
+ def run(self):
6
+ pass
@@ -1,4 +1,5 @@
1
1
  from nautobot.core.celery import register_jobs
2
+ from nautobot.extras.choices import JobResultStatusChoices
2
3
  from nautobot.extras.jobs import get_task_logger, Job
3
4
 
4
5
  logger = get_task_logger(__name__)
@@ -14,11 +15,50 @@ class TestPass(Job):
14
15
  class Meta:
15
16
  has_sensitive_variables = False
16
17
 
18
+ def before_start(self, task_id, args, kwargs):
19
+ if task_id != self.request.id:
20
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}")
21
+ if args:
22
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
23
+ if kwargs:
24
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
25
+ logger.info("before_start() was called as expected")
26
+
17
27
  def run(self):
18
28
  """
19
29
  Job function.
20
30
  """
21
31
  logger.info("Success")
32
+ return True
33
+
34
+ def on_success(self, retval, task_id, args, kwargs):
35
+ if retval is not True:
36
+ raise RuntimeError(f"Expected retval to be True, but it was {retval!r}")
37
+ if task_id != self.request.id:
38
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}")
39
+ if args:
40
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
41
+ if kwargs:
42
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
43
+ logger.info("on_success() was called as expected")
44
+
45
+ def on_failure(self, exc, task_id, args, kwargs, einfo):
46
+ raise RuntimeError("on_failure() was unexpectedly called!")
47
+
48
+ def after_return(self, status, retval, task_id, args, kwargs, einfo):
49
+ if status is not JobResultStatusChoices.STATUS_SUCCESS:
50
+ raise RuntimeError(f"Expected status to be {JobResultStatusChoices.STATUS_SUCCESS}, but it was {status!r}")
51
+ if retval is not True:
52
+ raise RuntimeError(f"Expected retval to be True, but it was {retval!r}")
53
+ if task_id != self.request.id:
54
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}")
55
+ if args:
56
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
57
+ if kwargs:
58
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
59
+ if einfo is not None:
60
+ raise RuntimeError(f"Expected einfo to be None, but it was {einfo!r}")
61
+ logger.info("after_return() was called as expected")
22
62
 
23
63
 
24
64
  register_jobs(TestPass)
@@ -0,0 +1,11 @@
1
+ from fail import TestFail # pylint: disable=import-error
2
+
3
+ from nautobot.apps.jobs import register_jobs
4
+
5
+
6
+ class TestReallyPass(TestFail):
7
+ def run(self):
8
+ pass
9
+
10
+
11
+ register_jobs(TestReallyPass)
@@ -2303,6 +2303,7 @@ class JobApprovalTest(APITestCase):
2303
2303
  name="test dryrun",
2304
2304
  task="dry_run.TestDryRun",
2305
2305
  job_model=cls.dryrun_job_model,
2306
+ kwargs={"value": 1},
2306
2307
  interval=JobExecutionType.TYPE_IMMEDIATELY,
2307
2308
  user=cls.additional_user,
2308
2309
  approval_required=True,
@@ -2442,6 +2443,8 @@ class JobApprovalTest(APITestCase):
2442
2443
  url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.dryrun_scheduled_job.pk})
2443
2444
  response = self.client.post(url, **self.header)
2444
2445
  self.assertHttpStatus(response, status.HTTP_200_OK)
2446
+ # The below fails because JobResult.task_kwargs doesn't get set until *after* the task begins executing.
2447
+ # self.assertEqual(response.data["task_kwargs"], {"dryrun": True, "value": 1}, response.data)
2445
2448
 
2446
2449
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2447
2450
  def test_dry_run_not_supported(self):
@@ -12,6 +12,7 @@ from nautobot.extras.context_managers import (
12
12
  web_request_context,
13
13
  )
14
14
  from nautobot.extras.models import Status, Webhook
15
+ from nautobot.extras.utils import bulk_delete_with_bulk_change_logging
15
16
 
16
17
  # Use the proper swappable User model
17
18
  User = get_user_model()
@@ -231,6 +232,23 @@ class BulkEditDeleteChangeLogging(TestCase):
231
232
  location.save()
232
233
  location.delete()
233
234
 
235
+ def test_bulk_delete_has_user_in_change_log(self):
236
+ """Test that the bulk delete operation adds the user to the change log"""
237
+ location_type = LocationType.objects.get(name="Campus")
238
+ location_status = Status.objects.get_for_model(Location).first()
239
+ with web_request_context(self.user):
240
+ location = Location(name="Test Location 1", location_type=location_type, status=location_status)
241
+ location.save()
242
+ location_pk = location.pk
243
+ location_qs = Location.objects.filter(pk=location_pk)
244
+ bulk_delete_with_bulk_change_logging(location_qs)
245
+
246
+ oc_list = get_changes_for_model(location)
247
+ self.assertEqual(len(oc_list), 2)
248
+ self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_DELETE)
249
+ self.assertEqual(oc_list[0].user, self.user)
250
+ self.assertEqual(oc_list[0].user_name, self.user.username)
251
+
234
252
  def test_create_then_update(self):
235
253
  """Test that a create followed by an update is logged as a single create"""
236
254
  location_type = LocationType.objects.get(name="Campus")