nautobot 2.3.1__py3-none-any.whl → 2.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. nautobot/core/celery/schedulers.py +18 -0
  2. nautobot/core/settings.yaml +3 -3
  3. nautobot/core/tables.py +1 -1
  4. nautobot/core/templates/home.html +4 -3
  5. nautobot/core/templatetags/buttons.py +1 -1
  6. nautobot/core/tests/runner.py +27 -9
  7. nautobot/core/tests/test_utils.py +13 -0
  8. nautobot/core/utils/lookup.py +7 -1
  9. nautobot/core/views/utils.py +3 -3
  10. nautobot/dcim/factory.py +3 -3
  11. nautobot/dcim/tables/devices.py +7 -7
  12. nautobot/dcim/templates/dcim/device.html +12 -0
  13. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +12 -0
  14. nautobot/dcim/utils.py +9 -6
  15. nautobot/extras/api/serializers.py +2 -0
  16. nautobot/extras/context_managers.py +11 -4
  17. nautobot/extras/filters/__init__.py +14 -2
  18. nautobot/extras/forms/forms.py +6 -0
  19. nautobot/extras/forms/mixins.py +2 -2
  20. nautobot/extras/jobs.py +0 -1
  21. nautobot/extras/management/__init__.py +3 -0
  22. nautobot/extras/migrations/0115_scheduledjob_time_zone.py +23 -0
  23. nautobot/extras/models/groups.py +4 -1
  24. nautobot/extras/models/jobs.py +24 -11
  25. nautobot/extras/tables.py +34 -4
  26. nautobot/extras/templates/extras/scheduledjob.html +13 -2
  27. nautobot/extras/tests/test_api.py +17 -18
  28. nautobot/extras/tests/test_context_managers.py +33 -14
  29. nautobot/extras/tests/test_dynamicgroups.py +11 -0
  30. nautobot/extras/tests/test_filters.py +57 -1
  31. nautobot/extras/tests/test_models.py +304 -1
  32. nautobot/extras/tests/test_views.py +4 -2
  33. nautobot/extras/views.py +7 -0
  34. nautobot/ipam/api/views.py +9 -2
  35. nautobot/ipam/choices.py +17 -0
  36. nautobot/ipam/factory.py +6 -0
  37. nautobot/ipam/filters.py +1 -1
  38. nautobot/ipam/forms.py +5 -3
  39. nautobot/ipam/migrations/0048_vrf_status.py +23 -0
  40. nautobot/ipam/migrations/0049_vrf_data_migration.py +25 -0
  41. nautobot/ipam/models.py +6 -0
  42. nautobot/ipam/tables.py +3 -2
  43. nautobot/ipam/templates/ipam/vrf.html +4 -0
  44. nautobot/ipam/templates/ipam/vrf_edit.html +1 -0
  45. nautobot/ipam/tests/test_api.py +44 -3
  46. nautobot/ipam/tests/test_views.py +3 -0
  47. nautobot/project-static/css/base.css +6 -0
  48. nautobot/project-static/docs/404.html +23 -23
  49. nautobot/project-static/docs/apps/index.html +25 -25
  50. nautobot/project-static/docs/apps/nautobot-apps.html +24 -24
  51. nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +16 -0
  52. nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js.map +7 -0
  53. nautobot/project-static/docs/assets/javascripts/workers/{search.b8dbb3d2.min.js → search.07f07601.min.js} +1 -1
  54. nautobot/project-static/docs/assets/javascripts/workers/{search.b8dbb3d2.min.js.map → search.07f07601.min.js.map} +1 -1
  55. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +1 -0
  56. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +1 -0
  57. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +26 -26
  58. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +26 -26
  59. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +58 -58
  60. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +32 -31
  61. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -31
  62. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +25 -25
  63. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +25 -25
  64. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +30 -30
  65. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +34 -34
  66. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +41 -41
  67. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +48 -48
  68. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +92 -92
  69. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +41 -41
  70. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +85 -85
  71. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +84 -84
  72. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +26 -26
  73. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +28 -28
  74. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +40 -40
  75. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +78 -78
  76. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +77 -77
  77. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +26 -26
  78. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +80 -80
  79. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +67 -67
  80. nautobot/project-static/docs/development/apps/api/configuration-view.html +25 -25
  81. nautobot/project-static/docs/development/apps/api/database-backend-config.html +25 -25
  82. nautobot/project-static/docs/development/apps/api/models/django-admin.html +25 -25
  83. nautobot/project-static/docs/development/apps/api/models/global-search.html +25 -25
  84. nautobot/project-static/docs/development/apps/api/models/graphql.html +25 -25
  85. nautobot/project-static/docs/development/apps/api/models/index.html +25 -25
  86. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +25 -25
  87. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +25 -25
  88. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +25 -25
  89. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +25 -25
  90. nautobot/project-static/docs/development/apps/api/platform-features/index.html +25 -25
  91. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +25 -25
  92. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +25 -25
  93. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +25 -25
  94. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +25 -25
  95. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +25 -25
  96. nautobot/project-static/docs/development/apps/api/prometheus.html +25 -25
  97. nautobot/project-static/docs/development/apps/api/setup.html +25 -25
  98. nautobot/project-static/docs/development/apps/api/testing.html +25 -25
  99. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +25 -25
  100. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +25 -25
  101. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +25 -25
  102. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +25 -25
  103. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +25 -25
  104. nautobot/project-static/docs/development/apps/api/views/base-template.html +25 -25
  105. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +25 -25
  106. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +25 -25
  107. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +25 -25
  108. nautobot/project-static/docs/development/apps/api/views/index.html +25 -25
  109. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +25 -25
  110. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +25 -25
  111. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +25 -25
  112. nautobot/project-static/docs/development/apps/api/views/notes.html +25 -25
  113. nautobot/project-static/docs/development/apps/api/views/rest-api.html +25 -25
  114. nautobot/project-static/docs/development/apps/api/views/urls.html +25 -25
  115. nautobot/project-static/docs/development/apps/index.html +25 -25
  116. nautobot/project-static/docs/development/apps/migration/code-updates.html +25 -25
  117. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +25 -25
  118. nautobot/project-static/docs/development/apps/migration/from-v1.html +25 -25
  119. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +25 -25
  120. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +25 -25
  121. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +25 -25
  122. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +25 -25
  123. nautobot/project-static/docs/development/apps/porting-from-netbox.html +25 -25
  124. nautobot/project-static/docs/development/core/application-registry.html +25 -25
  125. nautobot/project-static/docs/development/core/best-practices.html +25 -25
  126. nautobot/project-static/docs/development/core/bootstrap-ui.html +25 -25
  127. nautobot/project-static/docs/development/core/caching.html +25 -25
  128. nautobot/project-static/docs/development/core/controllers.html +25 -25
  129. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +25 -25
  130. nautobot/project-static/docs/development/core/generic-views.html +25 -25
  131. nautobot/project-static/docs/development/core/getting-started.html +25 -25
  132. nautobot/project-static/docs/development/core/homepage.html +25 -25
  133. nautobot/project-static/docs/development/core/index.html +25 -25
  134. nautobot/project-static/docs/development/core/model-checklist.html +25 -25
  135. nautobot/project-static/docs/development/core/model-features.html +25 -25
  136. nautobot/project-static/docs/development/core/natural-keys.html +25 -25
  137. nautobot/project-static/docs/development/core/navigation-menu.html +25 -25
  138. nautobot/project-static/docs/development/core/release-checklist.html +25 -25
  139. nautobot/project-static/docs/development/core/role-internals.html +25 -25
  140. nautobot/project-static/docs/development/core/settings.html +25 -25
  141. nautobot/project-static/docs/development/core/style-guide.html +25 -25
  142. nautobot/project-static/docs/development/core/templates.html +25 -25
  143. nautobot/project-static/docs/development/core/testing.html +25 -25
  144. nautobot/project-static/docs/development/core/user-preferences.html +25 -25
  145. nautobot/project-static/docs/development/index.html +25 -25
  146. nautobot/project-static/docs/development/jobs/index.html +25 -25
  147. nautobot/project-static/docs/development/jobs/migration/from-v1.html +25 -25
  148. nautobot/project-static/docs/index.html +30 -30
  149. nautobot/project-static/docs/overview/application_stack.html +31 -31
  150. nautobot/project-static/docs/overview/design_philosophy.html +25 -25
  151. nautobot/project-static/docs/release-notes/index.html +25 -25
  152. nautobot/project-static/docs/release-notes/version-1.0.html +25 -25
  153. nautobot/project-static/docs/release-notes/version-1.1.html +25 -25
  154. nautobot/project-static/docs/release-notes/version-1.2.html +25 -25
  155. nautobot/project-static/docs/release-notes/version-1.3.html +25 -25
  156. nautobot/project-static/docs/release-notes/version-1.4.html +25 -25
  157. nautobot/project-static/docs/release-notes/version-1.5.html +25 -25
  158. nautobot/project-static/docs/release-notes/version-1.6.html +25 -25
  159. nautobot/project-static/docs/release-notes/version-2.0.html +25 -25
  160. nautobot/project-static/docs/release-notes/version-2.1.html +25 -25
  161. nautobot/project-static/docs/release-notes/version-2.2.html +25 -25
  162. nautobot/project-static/docs/release-notes/version-2.3.html +318 -61
  163. nautobot/project-static/docs/search/search_index.json +1 -1
  164. nautobot/project-static/docs/sitemap.xml +271 -542
  165. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +25 -25
  167. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +25 -25
  168. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +25 -25
  169. nautobot/project-static/docs/user-guide/administration/configuration/index.html +25 -25
  170. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +28 -28
  171. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +25 -25
  172. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +25 -25
  173. nautobot/project-static/docs/user-guide/administration/guides/caching.html +25 -25
  174. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +25 -25
  175. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +25 -25
  176. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +25 -25
  177. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +25 -25
  178. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +25 -25
  179. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +25 -25
  180. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +25 -25
  181. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +25 -25
  182. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +25 -25
  183. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +25 -25
  184. nautobot/project-static/docs/user-guide/administration/installation/index.html +25 -25
  185. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +25 -25
  186. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +25 -25
  187. nautobot/project-static/docs/user-guide/administration/installation/services.html +25 -25
  188. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +25 -25
  189. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +25 -25
  190. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +25 -25
  191. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +25 -25
  192. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +25 -25
  193. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +25 -25
  194. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +25 -25
  195. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +25 -25
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +25 -25
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +25 -25
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +25 -25
  199. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +25 -25
  200. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +25 -25
  201. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +25 -25
  202. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +25 -25
  203. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +25 -25
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +25 -25
  205. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +25 -25
  206. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +25 -25
  207. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +25 -25
  208. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +25 -25
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +25 -25
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +25 -25
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +25 -25
  212. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +25 -25
  213. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +25 -25
  214. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +25 -25
  215. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +25 -25
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +25 -25
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +25 -25
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +25 -25
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +25 -25
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +25 -25
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +25 -25
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +25 -25
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +25 -25
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +25 -25
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +25 -25
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +25 -25
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +25 -25
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +25 -25
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +25 -25
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +25 -25
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +25 -25
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +25 -25
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +25 -25
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +25 -25
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +25 -25
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +25 -25
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +25 -25
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +25 -25
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +25 -25
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +25 -25
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +25 -25
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +25 -25
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +25 -25
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +25 -25
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +25 -25
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +25 -25
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +25 -25
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +25 -25
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +25 -25
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +25 -25
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +25 -25
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +25 -25
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +25 -25
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +25 -25
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +25 -25
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +25 -25
  257. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +25 -25
  258. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +25 -25
  259. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +25 -25
  260. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +25 -25
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +25 -25
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +25 -25
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +25 -25
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +25 -25
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +25 -25
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +25 -25
  267. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +25 -25
  268. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +25 -25
  269. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +25 -25
  270. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +25 -25
  271. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +25 -25
  272. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +25 -25
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +25 -25
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +25 -25
  275. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +25 -25
  276. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +25 -25
  277. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +25 -25
  278. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +25 -25
  279. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +25 -25
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +25 -25
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +25 -25
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +25 -25
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +25 -25
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +25 -25
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +25 -25
  286. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +25 -25
  287. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +25 -25
  288. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +25 -25
  289. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +25 -25
  290. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +25 -25
  291. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +25 -25
  292. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +25 -25
  293. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +25 -25
  294. nautobot/project-static/docs/user-guide/index.html +25 -25
  295. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +25 -25
  296. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +25 -25
  297. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +25 -25
  298. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +25 -25
  299. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +25 -25
  300. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +25 -25
  301. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +25 -25
  302. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +25 -25
  303. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +25 -25
  304. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +25 -25
  305. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +25 -25
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +25 -25
  307. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +25 -25
  308. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +25 -25
  309. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +25 -25
  310. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +25 -25
  311. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +25 -25
  312. nautobot/project-static/docs/user-guide/platform-functionality/note.html +25 -25
  313. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +25 -25
  314. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +25 -25
  315. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +25 -25
  316. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +25 -25
  317. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +25 -25
  318. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +25 -25
  319. nautobot/project-static/docs/user-guide/platform-functionality/role.html +25 -25
  320. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +25 -25
  321. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +25 -25
  322. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +25 -25
  323. nautobot/project-static/docs/user-guide/platform-functionality/status.html +25 -25
  324. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +25 -25
  325. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +25 -25
  326. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +25 -25
  327. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +25 -25
  328. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +25 -25
  329. nautobot/project-static/js/homepage_layout.js +3 -0
  330. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/METADATA +4 -4
  331. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/RECORD +335 -332
  332. nautobot/project-static/docs/assets/javascripts/bundle.fe8b6f2b.min.js +0 -29
  333. nautobot/project-static/docs/assets/javascripts/bundle.fe8b6f2b.min.js.map +0 -7
  334. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +0 -1
  335. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +0 -1
  336. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/LICENSE.txt +0 -0
  337. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/NOTICE +0 -0
  338. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/WHEEL +0 -0
  339. {nautobot-2.3.1.dist-info → nautobot-2.3.3.dist-info}/entry_points.txt +0 -0
@@ -4,7 +4,6 @@ import contextlib
4
4
  from datetime import timedelta
5
5
  import logging
6
6
 
7
- from celery import schedules
8
7
  from celery.exceptions import NotRegistered
9
8
  from celery.utils.log import get_logger, LoggingProxy
10
9
  from django.conf import settings
@@ -16,7 +15,9 @@ from django.db.models import signals
16
15
  from django.utils import timezone
17
16
  from django.utils.functional import cached_property
18
17
  from django_celery_beat.clockedschedule import clocked
18
+ from django_celery_beat.tzcrontab import TzAwareCrontab
19
19
  from prometheus_client import Histogram
20
+ from timezone_field import TimeZoneField
20
21
 
21
22
  from nautobot.core.celery import (
22
23
  app,
@@ -935,6 +936,9 @@ class ScheduledJob(BaseModel):
935
936
  verbose_name="Start Datetime",
936
937
  help_text="Datetime when the schedule should begin triggering the task to run",
937
938
  )
939
+ # Django always stores DateTimeField as UTC internally, but we want scheduled jobs to respect DST and similar,
940
+ # so we need to store the time zone the job was scheduled under as well.
941
+ time_zone = TimeZoneField(default=timezone.get_default_timezone_name)
938
942
  # todoindex:
939
943
  enabled = models.BooleanField(
940
944
  default=True,
@@ -1005,12 +1009,15 @@ class ScheduledJob(BaseModel):
1005
1009
  def __str__(self):
1006
1010
  return f"{self.name}: {self.interval}"
1007
1011
 
1012
+ class Meta:
1013
+ ordering = ["name"]
1014
+
1008
1015
  def save(self, *args, **kwargs):
1009
1016
  self.queue = self.queue or ""
1010
1017
  # make sure non-valid crontab doesn't get saved
1011
1018
  if self.interval == JobExecutionType.TYPE_CUSTOM:
1012
1019
  try:
1013
- self.get_crontab(self.crontab)
1020
+ self.get_crontab(self.crontab, tz=self.time_zone)
1014
1021
  except Exception as e:
1015
1022
  raise ValidationError({"crontab": e})
1016
1023
  if not self.enabled:
@@ -1055,7 +1062,7 @@ class ScheduledJob(BaseModel):
1055
1062
  return timezone.now() + timedelta(seconds=15)
1056
1063
 
1057
1064
  @classmethod
1058
- def get_crontab(cls, crontab):
1065
+ def get_crontab(cls, crontab, tz=None):
1059
1066
  """
1060
1067
  Wrapper method translates crontab syntax to Celery crontab.
1061
1068
 
@@ -1068,13 +1075,17 @@ class ScheduledJob(BaseModel):
1068
1075
 
1069
1076
  No support for Last (L), Weekday (W), Number symbol (#), Question mark (?), and special @ strings.
1070
1077
  """
1078
+ if not tz:
1079
+ tz = timezone.get_default_timezone()
1071
1080
  minute, hour, day_of_month, month_of_year, day_of_week = crontab.split(" ")
1072
- return schedules.crontab(
1081
+
1082
+ return TzAwareCrontab(
1073
1083
  minute=minute,
1074
1084
  hour=hour,
1075
1085
  day_of_month=day_of_month,
1076
1086
  month_of_year=month_of_year,
1077
1087
  day_of_week=day_of_week,
1088
+ tz=tz,
1078
1089
  )
1079
1090
 
1080
1091
  @classmethod
@@ -1116,13 +1127,13 @@ class ScheduledJob(BaseModel):
1116
1127
  """
1117
1128
 
1118
1129
  if interval == JobExecutionType.TYPE_IMMEDIATELY:
1119
- start_time = timezone.now()
1130
+ start_time = timezone.localtime()
1120
1131
  name = name or f"{job_model.name} - {start_time}"
1121
1132
  elif interval == JobExecutionType.TYPE_CUSTOM:
1122
1133
  if start_time is None:
1123
1134
  # "start_time" is checked against models.ScheduledJob.earliest_possible_time()
1124
1135
  # which returns timezone.now() + timedelta(seconds=15)
1125
- start_time = timezone.now() + timedelta(seconds=20)
1136
+ start_time = timezone.localtime() + timedelta(seconds=20)
1126
1137
 
1127
1138
  celery_kwargs = {
1128
1139
  "nautobot_job_profile": profile,
@@ -1145,6 +1156,7 @@ class ScheduledJob(BaseModel):
1145
1156
  task=job_model.class_path,
1146
1157
  job_model=job_model,
1147
1158
  start_time=start_time,
1159
+ time_zone=start_time.tzinfo,
1148
1160
  description=f"Nautobot job {name} scheduled by {user} for {start_time}",
1149
1161
  kwargs=job_kwargs,
1150
1162
  celery_kwargs=celery_kwargs,
@@ -1159,15 +1171,16 @@ class ScheduledJob(BaseModel):
1159
1171
  return scheduled_job
1160
1172
 
1161
1173
  def to_cron(self):
1162
- t = self.start_time
1174
+ tz = self.time_zone
1175
+ t = self.start_time.astimezone(tz)
1163
1176
  if self.interval == JobExecutionType.TYPE_HOURLY:
1164
- return schedules.crontab(minute=t.minute)
1177
+ return TzAwareCrontab(minute=t.minute, tz=tz)
1165
1178
  elif self.interval == JobExecutionType.TYPE_DAILY:
1166
- return schedules.crontab(minute=t.minute, hour=t.hour)
1179
+ return TzAwareCrontab(minute=t.minute, hour=t.hour, tz=tz)
1167
1180
  elif self.interval == JobExecutionType.TYPE_WEEKLY:
1168
- return schedules.crontab(minute=t.minute, hour=t.hour, day_of_week=t.strftime("%w"))
1181
+ return TzAwareCrontab(minute=t.minute, hour=t.hour, day_of_week=t.strftime("%w"), tz=tz)
1169
1182
  elif self.interval == JobExecutionType.TYPE_CUSTOM:
1170
- return self.get_crontab(self.crontab)
1183
+ return self.get_crontab(self.crontab, tz=tz)
1171
1184
  raise ValueError(f"I do not know to convert {self.interval} to a Cronjob!")
1172
1185
 
1173
1186
 
nautobot/extras/tables.py CHANGED
@@ -110,6 +110,10 @@ JOB_BUTTONS = """
110
110
  <a href="{% url 'extras:jobresult_list' %}?job_model={{ record.name | urlencode }}" class="btn btn-default btn-xs" title="Job Results"><i class="mdi mdi-format-list-bulleted" aria-hidden="true"></i></a>
111
111
  """
112
112
 
113
+ SCHEDULED_JOB_BUTTONS = """
114
+ <a href="{% url 'extras:jobresult_list' %}?scheduled_job={{ record.name | urlencode }}" class="btn btn-default btn-xs" title="Job Results"><i class="mdi mdi-format-list-bulleted" aria-hidden="true"></i></a>
115
+ """
116
+
113
117
  OBJECTCHANGE_OBJECT = """
114
118
  {% if record.changed_object and record.changed_object.get_absolute_url %}
115
119
  <a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
@@ -835,6 +839,10 @@ class JobResultTable(BaseTable):
835
839
  orderable=False,
836
840
  attrs={"td": {"class": "text-nowrap report-stats"}},
837
841
  )
842
+ scheduled_job = tables.Column(
843
+ linkify=True,
844
+ verbose_name="Scheduled Job",
845
+ )
838
846
  actions = tables.TemplateColumn(
839
847
  template_code="""
840
848
  {% load helpers %}
@@ -884,6 +892,7 @@ class JobResultTable(BaseTable):
884
892
  "date_created",
885
893
  "name",
886
894
  "job_model",
895
+ "scheduled_job",
887
896
  "duration",
888
897
  "date_done",
889
898
  "user",
@@ -1038,16 +1047,37 @@ class NoteTable(BaseTable):
1038
1047
 
1039
1048
  class ScheduledJobTable(BaseTable):
1040
1049
  pk = ToggleColumn()
1041
- name = tables.LinkColumn()
1050
+ name = tables.Column(linkify=True)
1042
1051
  job_model = tables.Column(verbose_name="Job", linkify=True)
1043
1052
  interval = tables.Column(verbose_name="Execution Type")
1044
- start_time = tables.Column(verbose_name="First Run")
1045
- last_run_at = tables.Column(verbose_name="Most Recent Run")
1053
+ start_time = tables.DateTimeColumn(verbose_name="First Run", format=settings.SHORT_DATETIME_FORMAT)
1054
+ last_run_at = tables.DateTimeColumn(verbose_name="Most Recent Run", format=settings.SHORT_DATETIME_FORMAT)
1055
+ crontab = tables.Column()
1046
1056
  total_run_count = tables.Column(verbose_name="Total Run Count")
1057
+ actions = ButtonsColumn(ScheduledJob, buttons=("delete"), prepend_template=SCHEDULED_JOB_BUTTONS)
1047
1058
 
1048
1059
  class Meta(BaseTable.Meta):
1049
1060
  model = ScheduledJob
1050
- fields = ("pk", "name", "job_model", "interval", "start_time", "last_run_at")
1061
+ fields = (
1062
+ "pk",
1063
+ "name",
1064
+ "total_run_count",
1065
+ "job_model",
1066
+ "interval",
1067
+ "start_time",
1068
+ "last_run_at",
1069
+ "crontab",
1070
+ "time_zone",
1071
+ "actions",
1072
+ )
1073
+ default_columns = (
1074
+ "pk",
1075
+ "name",
1076
+ "job_model",
1077
+ "interval",
1078
+ "last_run_at",
1079
+ "actions",
1080
+ )
1051
1081
 
1052
1082
 
1053
1083
  class ScheduledJobApprovalQueueTable(BaseTable):
@@ -4,6 +4,7 @@
4
4
  {% load helpers %}
5
5
  {% load perms %}
6
6
  {% load plugins %}
7
+ {% load tz %}
7
8
 
8
9
  {% block buttons %}
9
10
  {% plugin_buttons object %}
@@ -94,13 +95,23 @@
94
95
  <tr>
95
96
  <td>Start Time</td>
96
97
  <td>
97
- {{ object.start_time }}
98
+ {{ object.start_time|timezone:object.time_zone|date:'Y-m-d H:i:s T' }}
99
+ {% if default_time_zone != object.time_zone %}
100
+ <br>{{ object.start_time|timezone:default_time_zone|date:'Y-m-d H:i:s T' }}
101
+ {% endif %}
98
102
  </td>
99
103
  </tr>
100
104
  <tr>
101
105
  <td>Last Run At</td>
102
106
  <td>
103
- {{ object.last_run_at|placeholder }}
107
+ {% if object.last_run_at %}
108
+ {{ object.last_run_at|timezone:object.time_zone|date:'Y-m-d H:i:s T' }}
109
+ {% if default_time_zone != object.time_zone %}
110
+ <br>{{ object.last_run_at|timezone:default_time_zone|date:'Y-m-d H:i:s T' }}
111
+ {% endif %}
112
+ {% else %}
113
+ {{ object.last_run_at|placeholder }}
114
+ {% endif %}
104
115
  </td>
105
116
  </tr>
106
117
  <tr>
@@ -12,6 +12,11 @@ from django.urls import reverse
12
12
  from django.utils.timezone import make_aware, now
13
13
  from rest_framework import status
14
14
 
15
+ try:
16
+ from zoneinfo import ZoneInfo
17
+ except ImportError: # Python 3.8
18
+ from backports.zoneinfo import ZoneInfo
19
+
15
20
  from nautobot.core.choices import ColorChoices
16
21
  from nautobot.core.models.fields import slugify_dashes_to_underscores
17
22
  from nautobot.core.testing import APITestCase, APIViewTestCases
@@ -1583,7 +1588,7 @@ class JobTest(
1583
1588
  "schedule": {
1584
1589
  "name": "test",
1585
1590
  "interval": "future",
1586
- "start_time": str(datetime.now() + timedelta(minutes=1)),
1591
+ "start_time": str(now() + timedelta(minutes=1)),
1587
1592
  },
1588
1593
  }
1589
1594
 
@@ -1780,7 +1785,7 @@ class JobTest(
1780
1785
  "var2": "Ground control to Major Tom",
1781
1786
  "var23": "Commencing countdown, engines on",
1782
1787
  "var1": test_file,
1783
- "_schedule_start_time": str(datetime.now() + timedelta(minutes=1)),
1788
+ "_schedule_start_time": str(now() + timedelta(minutes=1)),
1784
1789
  "_schedule_interval": "future",
1785
1790
  "_schedule_name": "test",
1786
1791
  }
@@ -1799,7 +1804,7 @@ class JobTest(
1799
1804
  data = {
1800
1805
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1801
1806
  "schedule": {
1802
- "start_time": str(datetime.now() + timedelta(minutes=1)),
1807
+ "start_time": str(now() + timedelta(minutes=1)),
1803
1808
  "interval": "future",
1804
1809
  "name": "test",
1805
1810
  },
@@ -1838,7 +1843,7 @@ class JobTest(
1838
1843
  data = {
1839
1844
  "data": {},
1840
1845
  "schedule": {
1841
- "start_time": str(datetime.now() + timedelta(minutes=1)),
1846
+ "start_time": str(now() + timedelta(minutes=1)),
1842
1847
  "interval": "future",
1843
1848
  "name": "test",
1844
1849
  },
@@ -1914,7 +1919,7 @@ class JobTest(
1914
1919
  data = {
1915
1920
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1916
1921
  "schedule": {
1917
- "start_time": str(datetime.now() - timedelta(minutes=1)),
1922
+ "start_time": str(now() - timedelta(minutes=1)),
1918
1923
  "interval": "future",
1919
1924
  "name": "test",
1920
1925
  },
@@ -1933,7 +1938,7 @@ class JobTest(
1933
1938
  data = {
1934
1939
  "data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
1935
1940
  "schedule": {
1936
- "start_time": str(datetime.now() + timedelta(minutes=1)),
1941
+ "start_time": str(now() + timedelta(minutes=1)),
1937
1942
  "interval": "hourly",
1938
1943
  "name": "test",
1939
1944
  },
@@ -2438,30 +2443,24 @@ class ScheduledJobTest(
2438
2443
  name="test2",
2439
2444
  task="pass.TestPass",
2440
2445
  job_model=job_model,
2441
- interval=JobExecutionType.TYPE_IMMEDIATELY,
2446
+ interval=JobExecutionType.TYPE_DAILY,
2442
2447
  user=user,
2443
2448
  approval_required=True,
2444
- start_time=now(),
2449
+ start_time=datetime(2020, 1, 23, 12, 34, 56, tzinfo=ZoneInfo("America/New_York")),
2450
+ time_zone=ZoneInfo("America/New_York"),
2445
2451
  )
2446
2452
  ScheduledJob.objects.create(
2447
2453
  name="test3",
2448
2454
  task="pass.TestPass",
2449
2455
  job_model=job_model,
2450
- interval=JobExecutionType.TYPE_IMMEDIATELY,
2456
+ interval=JobExecutionType.TYPE_CUSTOM,
2457
+ crontab="34 12 * * *",
2458
+ enabled=False,
2451
2459
  user=user,
2452
2460
  approval_required=True,
2453
2461
  start_time=now(),
2454
2462
  )
2455
2463
 
2456
- # TODO: Unskip after resolving #2908, #2909
2457
- @skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
2458
- def test_list_objects_ascending_ordered(self):
2459
- pass
2460
-
2461
- @skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
2462
- def test_list_objects_descending_ordered(self):
2463
- pass
2464
-
2465
2464
 
2466
2465
  class JobApprovalTest(APITestCase):
2467
2466
  @classmethod
@@ -1,3 +1,5 @@
1
+ from unittest import mock
2
+
1
3
  from django.contrib.auth import get_user_model
2
4
  from django.contrib.contenttypes.models import ContentType
3
5
  from django.test import TestCase
@@ -72,7 +74,9 @@ class WebRequestContextTestCase(TestCase):
72
74
  self.assertEqual(oc_list[0].changed_object, location)
73
75
  self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
74
76
 
75
- def test_create_then_delete(self):
77
+ @mock.patch("nautobot.extras.jobs.enqueue_job_hooks")
78
+ @mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
79
+ def test_create_then_delete(self, mock_enqueue_webhooks, mock_enqueue_job_hooks):
76
80
  """Test that a create followed by a delete is logged as two changes"""
77
81
  location_type = LocationType.objects.get(name="Campus")
78
82
  location_status = Status.objects.get_for_model(Location).first()
@@ -88,6 +92,8 @@ class WebRequestContextTestCase(TestCase):
88
92
  self.assertEqual(len(oc_list), 2)
89
93
  self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_DELETE)
90
94
  self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_CREATE)
95
+ mock_enqueue_job_hooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
96
+ mock_enqueue_webhooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
91
97
 
92
98
  def test_update_then_delete(self):
93
99
  """Test that an update followed by a delete is logged as a single delete"""
@@ -179,21 +185,30 @@ class WebRequestContextTestCase(TestCase):
179
185
  with self.subTest():
180
186
  self.assertEqual(oc_list[0].change_context_detail, "test_change_log_context")
181
187
 
182
- def test_change_webhook_enqueued(self):
188
+ @mock.patch("nautobot.extras.webhooks.process_webhook.apply_async")
189
+ def test_change_webhook_enqueued(self, mock_apply_async):
183
190
  """Test that the webhook resides on the queue"""
184
- # TODO(john): come back to this with a way to actually do it without a running worker
185
- # The celery inspection API expects to be able to communicate with at least 1 running
186
- # worker and there does not appear to be an easy way to look into the queues directly.
187
- # with web_request_context(self.user):
188
- # site = Site(name="Test Site 2")
189
- # site.save()
191
+ with web_request_context(self.user):
192
+ location = Location(
193
+ name="Test Location 2",
194
+ location_type=LocationType.objects.get(name="Campus"),
195
+ status=Status.objects.get_for_model(Location).first(),
196
+ )
197
+ location.save()
190
198
 
191
199
  # Verify that a job was queued for the object creation webhook
192
- # site = Site.objects.get(name="Test Site 2")
193
-
194
- # self.assertEqual(job.args[0], Webhook.objects.get(type_create=True))
195
- # self.assertEqual(job.args[1]["id"], str(site.pk))
196
- # self.assertEqual(job.args[2], "site")
200
+ oc_list = get_changes_for_model(location)
201
+ mock_apply_async.assert_called_once()
202
+ call_args = mock_apply_async.call_args.kwargs["args"]
203
+ self.assertEqual(8, len(call_args), call_args)
204
+ self.assertEqual(call_args[0], Webhook.objects.get(type_create=True).pk)
205
+ self.assertEqual(call_args[1], oc_list[0].object_data_v2)
206
+ self.assertEqual(call_args[2], "location")
207
+ self.assertEqual(call_args[3], "create")
208
+ self.assertIsInstance(call_args[4], str) # str(timezone.now())
209
+ self.assertEqual(call_args[5], self.user.username)
210
+ self.assertEqual(call_args[6], oc_list[0].request_id)
211
+ self.assertEqual(call_args[7], oc_list[0].get_snapshots())
197
212
 
198
213
 
199
214
  class WebRequestContextTransactionTestCase(TransactionTestCase):
@@ -282,7 +297,8 @@ class BulkEditDeleteChangeLogging(TestCase):
282
297
  self.assertIsNone(snapshots["differences"]["removed"])
283
298
  self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
284
299
 
285
- def test_bulk_edit(self):
300
+ @mock.patch("nautobot.extras.jobs.import_jobs")
301
+ def test_bulk_edit(self, mock_import_jobs):
286
302
  """Test that edits to multiple objects are correctly logged"""
287
303
  location_type = LocationType.objects.get(name="Campus")
288
304
  location_status = Status.objects.get_for_model(Location).first()
@@ -309,6 +325,9 @@ class BulkEditDeleteChangeLogging(TestCase):
309
325
  self.assertIsNone(snapshots["differences"]["removed"])
310
326
  self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
311
327
 
328
+ # Check for regression of https://github.com/nautobot/nautobot/issues/6203
329
+ mock_import_jobs.assert_called_once()
330
+
312
331
  def test_bulk_edit_device_type_software_image_file(self):
313
332
  """Test that bulk edits to null does not cause integrity error"""
314
333
  manufacturer = Manufacturer.objects.create(name="Test")
@@ -494,6 +494,17 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
494
494
  self.assertIsInstance(fields["cluster"], MultiMatchModelMultipleChoiceField)
495
495
  self.assertIsInstance(fields["cluster"].widget, APISelectMultiple)
496
496
 
497
+ def test_map_filter_fields_with_custom_filter_method(self):
498
+ """
499
+ Test that extension_filters with custom methods can be concatenated into `generate_query_{filter_method}`
500
+ and _map_filter_fields method doesn't brake on string concatenation.
501
+ This is a regression test for issue #6184.
502
+ """
503
+ tenant_content_type = ContentType.objects.get_for_model(Tenant)
504
+ # using Tenant as example app has a custom filter with a custom method.
505
+ group = DynamicGroup(name="tenant", content_type=tenant_content_type)
506
+ self.assertIsInstance(group._map_filter_fields, dict)
507
+
497
508
  def test_map_filter_fields_skip_missing(self):
498
509
  """
499
510
  Test that missing fields are skipped in `DynamicGroup._map_filter_fields`
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  import uuid
2
3
 
3
4
  from django.contrib.auth import get_user_model
@@ -5,6 +6,12 @@ from django.contrib.contenttypes.models import ContentType
5
6
  from django.core.files.uploadedfile import SimpleUploadedFile
6
7
  from django.db.models import Q
7
8
  from django.test import override_settings, RequestFactory
9
+ from django.utils.timezone import now
10
+
11
+ try:
12
+ from zoneinfo import ZoneInfo
13
+ except ImportError: # Python 3.8
14
+ from backports.zoneinfo import ZoneInfo
8
15
 
9
16
  from nautobot.core.choices import ColorChoices
10
17
  from nautobot.core.testing import FilterTestCases
@@ -22,6 +29,7 @@ from nautobot.dcim.models import (
22
29
  from nautobot.extras.choices import (
23
30
  CustomFieldTypeChoices,
24
31
  DynamicGroupTypeChoices,
32
+ JobExecutionType,
25
33
  JobResultStatusChoices,
26
34
  MetadataTypeDataTypeChoices,
27
35
  ObjectChangeActionChoices,
@@ -93,6 +101,7 @@ from nautobot.extras.models import (
93
101
  RelationshipAssociation,
94
102
  Role,
95
103
  SavedView,
104
+ ScheduledJob,
96
105
  Secret,
97
106
  SecretsGroup,
98
107
  SecretsGroupAssociation,
@@ -1124,13 +1133,49 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
1124
1133
  def setUpTestData(cls):
1125
1134
  jobs = Job.objects.all()[:3]
1126
1135
  cls.jobs = jobs
1136
+ user = User.objects.create(username="user1", is_active=True)
1137
+ job_model = Job.objects.get_for_class_path("pass.TestPass")
1138
+ scheduled_jobs = [
1139
+ ScheduledJob.objects.create(
1140
+ name="test1",
1141
+ task="pass.TestPass",
1142
+ job_model=job_model,
1143
+ interval=JobExecutionType.TYPE_IMMEDIATELY,
1144
+ user=user,
1145
+ approval_required=True,
1146
+ start_time=now(),
1147
+ ),
1148
+ ScheduledJob.objects.create(
1149
+ name="test2",
1150
+ task="pass.TestPass",
1151
+ job_model=job_model,
1152
+ interval=JobExecutionType.TYPE_DAILY,
1153
+ user=user,
1154
+ approval_required=True,
1155
+ start_time=datetime(2020, 1, 23, 12, 34, 56, tzinfo=ZoneInfo("America/New_York")),
1156
+ time_zone=ZoneInfo("America/New_York"),
1157
+ ),
1158
+ ScheduledJob.objects.create(
1159
+ name="test3",
1160
+ task="pass.TestPass",
1161
+ job_model=job_model,
1162
+ interval=JobExecutionType.TYPE_CUSTOM,
1163
+ crontab="34 12 * * *",
1164
+ enabled=False,
1165
+ user=user,
1166
+ approval_required=True,
1167
+ start_time=now(),
1168
+ ),
1169
+ ]
1170
+ cls.scheduled_jobs = scheduled_jobs
1127
1171
  user = UserFactory.create()
1128
- for job in jobs:
1172
+ for idx, job in enumerate(jobs):
1129
1173
  JobResult.objects.create(
1130
1174
  job_model=job,
1131
1175
  name=job.class_path,
1132
1176
  user=user,
1133
1177
  status=JobResultStatusChoices.STATUS_STARTED,
1178
+ scheduled_job=scheduled_jobs[idx],
1134
1179
  )
1135
1180
 
1136
1181
  def test_job_model(self):
@@ -1144,6 +1189,17 @@ class JobResultFilterSetTestCase(FilterTestCases.FilterTestCase):
1144
1189
  self.filterset(params, self.queryset).qs, self.queryset.filter(job_model__in=jobs).distinct()
1145
1190
  )
1146
1191
 
1192
+ def test_scheduled_job(self):
1193
+ scheduled_jobs = list(self.scheduled_jobs[:2])
1194
+ filter_params = [
1195
+ {"scheduled_job": [scheduled_jobs[0].pk, scheduled_jobs[1].name]},
1196
+ ]
1197
+ for params in filter_params:
1198
+ self.assertQuerysetEqualAndNotEmpty(
1199
+ self.filterset(params, self.queryset).qs,
1200
+ self.queryset.filter(scheduled_job__in=scheduled_jobs).distinct(),
1201
+ )
1202
+
1147
1203
 
1148
1204
  class JobHookFilterSetTestCase(FilterTestCases.NameOnlyFilterTestCase):
1149
1205
  queryset = JobHook.objects.all()