nautobot 2.3.0b1__py3-none-any.whl → 2.3.2__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 (399) hide show
  1. nautobot/cloud/factory.py +2 -0
  2. nautobot/cloud/filters.py +3 -0
  3. nautobot/cloud/forms.py +8 -2
  4. nautobot/cloud/migrations/0001_initial.py +1 -1
  5. nautobot/cloud/models.py +1 -2
  6. nautobot/cloud/tables.py +1 -17
  7. nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +1 -7
  8. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +11 -0
  9. nautobot/cloud/templates/cloud/cloudservice_retrieve.html +4 -0
  10. nautobot/cloud/tests/test_filters.py +12 -0
  11. nautobot/cloud/tests/test_views.py +17 -0
  12. nautobot/cloud/views.py +1 -1
  13. nautobot/core/celery/__init__.py +5 -2
  14. nautobot/core/celery/schedulers.py +18 -0
  15. nautobot/core/filters.py +15 -1
  16. nautobot/core/forms/forms.py +10 -2
  17. nautobot/core/graphql/generators.py +2 -2
  18. nautobot/core/graphql/schema.py +6 -14
  19. nautobot/core/jobs/__init__.py +4 -1
  20. nautobot/core/management/commands/generate_test_data.py +2 -2
  21. nautobot/core/models/__init__.py +2 -2
  22. nautobot/core/settings.py +13 -2
  23. nautobot/core/settings.yaml +19 -5
  24. nautobot/core/tables.py +4 -1
  25. nautobot/core/templates/generic/object_retrieve.html +6 -6
  26. nautobot/core/templates/home.html +4 -3
  27. nautobot/core/templates/inc/computed_fields/panel_data.html +36 -24
  28. nautobot/core/templates/inc/object_details_advanced_panel.html +1 -1
  29. nautobot/core/templates/nautobot_config.py.j2 +15 -0
  30. nautobot/core/templatetags/buttons.py +1 -1
  31. nautobot/core/testing/filters.py +12 -1
  32. nautobot/core/tests/integration/test_general_functionality.py +1 -1
  33. nautobot/core/tests/test_jobs.py +74 -1
  34. nautobot/core/views/__init__.py +1 -1
  35. nautobot/core/views/generic.py +1 -1
  36. nautobot/core/views/mixins.py +1 -1
  37. nautobot/core/views/utils.py +11 -9
  38. nautobot/dcim/factory.py +7 -4
  39. nautobot/dcim/filters/__init__.py +4 -0
  40. nautobot/dcim/forms.py +24 -0
  41. nautobot/dcim/migrations/0061_module_models.py +1 -0
  42. nautobot/dcim/models/device_components.py +7 -0
  43. nautobot/dcim/models/devices.py +18 -19
  44. nautobot/dcim/models/racks.py +0 -1
  45. nautobot/dcim/tables/devices.py +24 -10
  46. nautobot/dcim/tables/devicetypes.py +1 -1
  47. nautobot/dcim/templates/dcim/device/base.html +1 -1
  48. nautobot/dcim/templates/dcim/device.html +15 -3
  49. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +6 -0
  50. nautobot/dcim/templates/dcim/moduletype_retrieve.html +17 -0
  51. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +15 -3
  52. nautobot/dcim/tests/test_api.py +2 -0
  53. nautobot/dcim/tests/test_filters.py +14 -7
  54. nautobot/dcim/tests/test_forms.py +54 -0
  55. nautobot/dcim/tests/test_models.py +40 -1
  56. nautobot/dcim/tests/test_views.py +45 -2
  57. nautobot/dcim/utils.py +9 -6
  58. nautobot/dcim/views.py +4 -1
  59. nautobot/extras/api/serializers.py +2 -1
  60. nautobot/extras/api/views.py +7 -59
  61. nautobot/extras/factory.py +50 -12
  62. nautobot/extras/filters/__init__.py +18 -3
  63. nautobot/extras/forms/base.py +10 -4
  64. nautobot/extras/forms/forms.py +7 -0
  65. nautobot/extras/forms/mixins.py +2 -2
  66. nautobot/extras/homepage.py +12 -2
  67. nautobot/extras/jobs.py +2 -2
  68. nautobot/extras/management/__init__.py +3 -0
  69. nautobot/extras/migrations/0111_metadata.py +4 -4
  70. nautobot/extras/migrations/0114_computedfield_grouping.py +17 -0
  71. nautobot/extras/migrations/0115_scheduledjob_time_zone.py +23 -0
  72. nautobot/extras/models/customfields.py +54 -0
  73. nautobot/extras/models/jobs.py +105 -9
  74. nautobot/extras/models/metadata.py +18 -18
  75. nautobot/extras/models/models.py +2 -0
  76. nautobot/extras/signals.py +14 -1
  77. nautobot/extras/tables.py +77 -18
  78. nautobot/extras/templates/extras/computedfield.html +4 -0
  79. nautobot/extras/templates/extras/job_detail.html +11 -0
  80. nautobot/extras/templates/extras/scheduledjob.html +13 -2
  81. nautobot/extras/tests/test_api.py +33 -27
  82. nautobot/extras/tests/test_filters.py +57 -1
  83. nautobot/extras/tests/test_jobs.py +2 -2
  84. nautobot/extras/tests/test_models.py +319 -19
  85. nautobot/extras/tests/test_views.py +26 -5
  86. nautobot/extras/utils.py +35 -6
  87. nautobot/extras/views.py +35 -51
  88. nautobot/ipam/api/views.py +9 -2
  89. nautobot/ipam/choices.py +17 -0
  90. nautobot/ipam/factory.py +6 -0
  91. nautobot/ipam/filters.py +2 -2
  92. nautobot/ipam/forms.py +6 -4
  93. nautobot/ipam/migrations/0048_vrf_status.py +23 -0
  94. nautobot/ipam/migrations/0049_vrf_data_migration.py +25 -0
  95. nautobot/ipam/models.py +11 -20
  96. nautobot/ipam/querysets.py +26 -0
  97. nautobot/ipam/tables.py +7 -2
  98. nautobot/ipam/templates/ipam/vrf.html +4 -0
  99. nautobot/ipam/templates/ipam/vrf_edit.html +1 -0
  100. nautobot/ipam/tests/test_api.py +33 -3
  101. nautobot/ipam/tests/test_models.py +89 -2
  102. nautobot/ipam/tests/test_views.py +3 -0
  103. nautobot/ipam/views.py +10 -15
  104. nautobot/project-static/css/base.css +7 -0
  105. nautobot/project-static/docs/404.html +18 -18
  106. nautobot/project-static/docs/apps/index.html +18 -18
  107. nautobot/project-static/docs/apps/nautobot-apps.html +18 -18
  108. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css +1 -0
  109. nautobot/project-static/docs/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  110. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +18 -18
  111. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +18 -18
  112. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +66 -18
  113. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +18 -18
  114. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +18 -18
  115. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +18 -18
  116. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +18 -18
  117. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +18 -18
  118. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +66 -18
  119. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +34 -18
  120. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +82 -63
  121. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +75 -111
  122. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +18 -18
  123. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +34 -18
  124. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +161 -18
  125. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +18 -18
  126. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +18 -18
  127. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -18
  128. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +18 -18
  129. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +18 -18
  130. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +18 -18
  131. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +21 -19
  132. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +34 -18
  133. nautobot/project-static/docs/development/apps/api/configuration-view.html +18 -18
  134. nautobot/project-static/docs/development/apps/api/database-backend-config.html +18 -18
  135. nautobot/project-static/docs/development/apps/api/models/django-admin.html +18 -18
  136. nautobot/project-static/docs/development/apps/api/models/global-search.html +18 -18
  137. nautobot/project-static/docs/development/apps/api/models/graphql.html +18 -18
  138. nautobot/project-static/docs/development/apps/api/models/index.html +33 -22
  139. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +18 -18
  140. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +18 -18
  141. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +18 -18
  142. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +18 -18
  143. nautobot/project-static/docs/development/apps/api/platform-features/index.html +18 -18
  144. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +18 -18
  145. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +18 -18
  146. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +18 -18
  147. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +18 -18
  148. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +18 -18
  149. nautobot/project-static/docs/development/apps/api/prometheus.html +18 -18
  150. nautobot/project-static/docs/development/apps/api/setup.html +18 -18
  151. nautobot/project-static/docs/development/apps/api/testing.html +18 -18
  152. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +18 -18
  153. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +18 -18
  154. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +18 -18
  155. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +18 -18
  156. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +18 -18
  157. nautobot/project-static/docs/development/apps/api/views/base-template.html +18 -18
  158. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +18 -18
  159. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +18 -18
  160. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +18 -18
  161. nautobot/project-static/docs/development/apps/api/views/index.html +18 -18
  162. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +18 -18
  163. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +18 -18
  164. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +18 -18
  165. nautobot/project-static/docs/development/apps/api/views/notes.html +18 -18
  166. nautobot/project-static/docs/development/apps/api/views/rest-api.html +18 -18
  167. nautobot/project-static/docs/development/apps/api/views/urls.html +18 -18
  168. nautobot/project-static/docs/development/apps/index.html +18 -18
  169. nautobot/project-static/docs/development/apps/migration/code-updates.html +18 -18
  170. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +18 -18
  171. nautobot/project-static/docs/development/apps/migration/from-v1.html +18 -18
  172. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +18 -18
  173. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +18 -18
  174. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +18 -18
  175. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +18 -18
  176. nautobot/project-static/docs/development/apps/porting-from-netbox.html +18 -18
  177. nautobot/project-static/docs/development/core/application-registry.html +18 -18
  178. nautobot/project-static/docs/development/core/best-practices.html +18 -18
  179. nautobot/project-static/docs/development/core/bootstrap-ui.html +18 -18
  180. nautobot/project-static/docs/development/core/caching.html +18 -18
  181. nautobot/project-static/docs/development/core/controllers.html +18 -18
  182. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +18 -18
  183. nautobot/project-static/docs/development/core/generic-views.html +18 -18
  184. nautobot/project-static/docs/development/core/getting-started.html +18 -18
  185. nautobot/project-static/docs/development/core/homepage.html +18 -18
  186. nautobot/project-static/docs/development/core/index.html +29 -18
  187. nautobot/project-static/docs/development/core/model-checklist.html +26 -20
  188. nautobot/project-static/docs/development/core/model-features.html +18 -18
  189. nautobot/project-static/docs/development/core/natural-keys.html +18 -18
  190. nautobot/project-static/docs/development/core/navigation-menu.html +18 -18
  191. nautobot/project-static/docs/development/core/release-checklist.html +18 -18
  192. nautobot/project-static/docs/development/core/role-internals.html +18 -18
  193. nautobot/project-static/docs/development/core/settings.html +18 -18
  194. nautobot/project-static/docs/development/core/style-guide.html +19 -19
  195. nautobot/project-static/docs/development/core/templates.html +18 -18
  196. nautobot/project-static/docs/development/core/testing.html +18 -18
  197. nautobot/project-static/docs/development/core/user-preferences.html +18 -18
  198. nautobot/project-static/docs/development/index.html +18 -18
  199. nautobot/project-static/docs/development/jobs/index.html +393 -379
  200. nautobot/project-static/docs/development/jobs/migration/from-v1.html +18 -18
  201. nautobot/project-static/docs/index.html +9032 -13
  202. nautobot/project-static/docs/models/extras/metadatachoice.html +3 -3
  203. nautobot/project-static/docs/models/extras/metadatatype.html +3 -3
  204. nautobot/project-static/docs/models/extras/objectmetadata.html +3 -3
  205. nautobot/project-static/docs/objects.inv +0 -0
  206. nautobot/project-static/docs/overview/application_stack.html +18 -18
  207. nautobot/project-static/docs/overview/design_philosophy.html +20 -20
  208. nautobot/project-static/docs/overview/index.html +13 -9032
  209. nautobot/project-static/docs/release-notes/index.html +252 -19
  210. nautobot/project-static/docs/release-notes/version-1.0.html +18 -18
  211. nautobot/project-static/docs/release-notes/version-1.1.html +18 -18
  212. nautobot/project-static/docs/release-notes/version-1.2.html +18 -18
  213. nautobot/project-static/docs/release-notes/version-1.3.html +18 -18
  214. nautobot/project-static/docs/release-notes/version-1.4.html +18 -18
  215. nautobot/project-static/docs/release-notes/version-1.5.html +18 -18
  216. nautobot/project-static/docs/release-notes/version-1.6.html +18 -18
  217. nautobot/project-static/docs/release-notes/version-2.0.html +18 -18
  218. nautobot/project-static/docs/release-notes/version-2.1.html +18 -18
  219. nautobot/project-static/docs/release-notes/version-2.2.html +248 -111
  220. nautobot/project-static/docs/release-notes/version-2.3.html +775 -91
  221. nautobot/project-static/docs/requirements.txt +3 -3
  222. nautobot/project-static/docs/search/search_index.json +1 -1
  223. nautobot/project-static/docs/sitemap.xml +278 -278
  224. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  225. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +18 -18
  226. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +18 -18
  227. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +18 -18
  228. nautobot/project-static/docs/user-guide/administration/configuration/index.html +18 -18
  229. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +55 -23
  230. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +18 -18
  231. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +18 -18
  232. nautobot/project-static/docs/user-guide/administration/guides/caching.html +18 -18
  233. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +22 -18
  234. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +18 -18
  235. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +18 -18
  236. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +18 -18
  237. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +18 -18
  238. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +18 -18
  239. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +18 -18
  240. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +18 -18
  241. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +18 -18
  242. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +69 -82
  243. nautobot/project-static/docs/user-guide/administration/installation/index.html +24 -24
  244. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +60 -52
  245. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +80 -87
  246. nautobot/project-static/docs/user-guide/administration/installation/services.html +37 -44
  247. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +18 -18
  248. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +18 -18
  249. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +18 -18
  250. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +18 -18
  251. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +18 -18
  252. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +76 -24
  253. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +18 -18
  254. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +18 -18
  255. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +18 -18
  256. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +18 -18
  257. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +18 -18
  258. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +18 -18
  259. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +18 -18
  260. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +18 -18
  261. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +18 -18
  262. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +18 -18
  263. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +18 -18
  264. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +18 -18
  265. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +18 -18
  266. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +18 -18
  267. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +18 -18
  268. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +18 -18
  269. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +18 -18
  270. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +18 -18
  271. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +18 -18
  272. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +18 -18
  273. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +18 -18
  274. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +18 -18
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +18 -18
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +18 -18
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +18 -18
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +18 -18
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +18 -18
  280. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +18 -18
  281. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +18 -18
  282. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +19 -19
  283. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +18 -18
  284. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +18 -18
  285. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +18 -18
  286. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +18 -18
  287. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +18 -18
  288. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +18 -18
  289. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +18 -18
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +18 -18
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +18 -18
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +18 -18
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +18 -18
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +18 -18
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +18 -18
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +19 -19
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +18 -18
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +18 -18
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +18 -18
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +18 -18
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +18 -18
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +18 -18
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +18 -18
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +18 -18
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +18 -18
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +18 -18
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +18 -18
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +18 -18
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +18 -18
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +18 -18
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +18 -18
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +18 -18
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +18 -18
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +18 -18
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +18 -18
  316. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +62 -18
  317. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +18 -18
  318. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +18 -18
  319. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +18 -18
  320. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +18 -18
  321. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +18 -18
  322. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +18 -18
  323. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +18 -18
  324. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +18 -18
  325. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +18 -18
  326. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +18 -18
  327. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +18 -18
  328. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +18 -18
  329. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +18 -18
  330. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +18 -18
  331. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +18 -18
  332. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +18 -18
  333. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +18 -18
  334. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +18 -18
  335. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +18 -18
  336. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +18 -18
  337. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +18 -18
  338. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +18 -18
  339. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +18 -18
  340. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +18 -18
  341. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +18 -18
  342. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +18 -18
  343. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +18 -18
  344. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +18 -18
  345. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +18 -18
  346. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +18 -18
  347. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +18 -18
  348. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +18 -18
  349. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +18 -18
  350. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +18 -18
  351. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +18 -18
  352. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +18 -18
  353. nautobot/project-static/docs/user-guide/index.html +18 -18
  354. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +18 -18
  355. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +18 -18
  356. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +18 -18
  357. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +18 -18
  358. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +18 -18
  359. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +18 -18
  360. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +18 -18
  361. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +18 -18
  362. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +18 -18
  363. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +18 -18
  364. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +18 -18
  365. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +18 -18
  366. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +21 -21
  367. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +18 -18
  368. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +18 -18
  369. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +18 -18
  370. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +36 -36
  371. nautobot/project-static/docs/user-guide/platform-functionality/note.html +33 -33
  372. nautobot/project-static/docs/user-guide/platform-functionality/{metadata.html → objectmetadata.html} +197 -84
  373. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +21 -21
  374. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +18 -18
  375. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +18 -18
  376. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +18 -18
  377. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +18 -18
  378. nautobot/project-static/docs/user-guide/platform-functionality/role.html +18 -18
  379. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +18 -18
  380. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +18 -18
  381. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +18 -18
  382. nautobot/project-static/docs/user-guide/platform-functionality/status.html +18 -18
  383. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +18 -18
  384. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +18 -18
  385. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +18 -18
  386. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +18 -18
  387. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +18 -18
  388. nautobot/project-static/js/homepage_layout.js +3 -0
  389. nautobot/tenancy/templates/tenancy/tenant.html +4 -4
  390. nautobot/virtualization/models.py +0 -2
  391. nautobot/virtualization/tables.py +2 -5
  392. {nautobot-2.3.0b1.dist-info → nautobot-2.3.2.dist-info}/METADATA +3 -3
  393. {nautobot-2.3.0b1.dist-info → nautobot-2.3.2.dist-info}/RECORD +397 -393
  394. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css +0 -1
  395. nautobot/project-static/docs/assets/stylesheets/main.76a95c52.min.css.map +0 -1
  396. {nautobot-2.3.0b1.dist-info → nautobot-2.3.2.dist-info}/LICENSE.txt +0 -0
  397. {nautobot-2.3.0b1.dist-info → nautobot-2.3.2.dist-info}/NOTICE +0 -0
  398. {nautobot-2.3.0b1.dist-info → nautobot-2.3.2.dist-info}/WHEEL +0 -0
  399. {nautobot-2.3.0b1.dist-info → nautobot-2.3.2.dist-info}/entry_points.txt +0 -0
@@ -14,8 +14,15 @@ from django.db.models import ProtectedError
14
14
  from django.db.utils import IntegrityError
15
15
  from django.test import override_settings
16
16
  from django.test.utils import isolate_apps
17
- from django.utils.timezone import now
17
+ from django.utils.timezone import get_default_timezone, now
18
+ from django_celery_beat.tzcrontab import TzAwareCrontab
18
19
  from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
20
+ import time_machine
21
+
22
+ try:
23
+ from zoneinfo import ZoneInfo
24
+ except ImportError: # python 3.8
25
+ from backports.zoneinfo import ZoneInfo
19
26
 
20
27
  from nautobot.circuits.models import CircuitType
21
28
  from nautobot.core.choices import ColorChoices
@@ -30,6 +37,7 @@ from nautobot.dcim.models import (
30
37
  Platform,
31
38
  )
32
39
  from nautobot.extras.choices import (
40
+ JobExecutionType,
33
41
  JobResultStatusChoices,
34
42
  LogLevelChoices,
35
43
  MetadataTypeDataTypeChoices,
@@ -65,6 +73,7 @@ from nautobot.extras.models import (
65
73
  ObjectMetadata,
66
74
  Role,
67
75
  SavedView,
76
+ ScheduledJob,
68
77
  Secret,
69
78
  SecretsGroup,
70
79
  SecretsGroupAssociation,
@@ -1352,7 +1361,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1352
1361
  value="Invalid assigned object type",
1353
1362
  scoped_fields=["status"],
1354
1363
  assigned_object_type=ContentType.objects.get_for_model(IPAddress),
1355
- assigned_object_id=Contact.objects.first().pk,
1364
+ assigned_object_id=Contact.objects.filter(associated_object_metadata__isnull=True).first().pk,
1356
1365
  )
1357
1366
  obj_metadata.validated_save()
1358
1367
 
@@ -1362,29 +1371,29 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1362
1371
  )
1363
1372
  type_contact_team.content_types.add(ContentType.objects.get_for_model(Contact))
1364
1373
  type_contact_team.content_types.add(ContentType.objects.get_for_model(Team))
1365
- instance1 = ObjectMetadata.objects.create(
1374
+ instance1 = ObjectMetadata(
1366
1375
  metadata_type=type_contact_team,
1367
1376
  contact=Contact.objects.first(),
1368
1377
  team=Team.objects.first(),
1369
1378
  scoped_fields=["address"],
1370
1379
  assigned_object_type=ContentType.objects.get_for_model(Contact),
1371
- assigned_object_id=Contact.objects.first().pk,
1380
+ assigned_object_id=Contact.objects.filter(associated_object_metadata__isnull=True).first().pk,
1372
1381
  )
1373
- instance2 = ObjectMetadata.objects.create(
1382
+ instance2 = ObjectMetadata(
1374
1383
  metadata_type=type_contact_team,
1375
1384
  contact=None,
1376
1385
  team=None,
1377
1386
  scoped_fields=["phone"],
1378
1387
  assigned_object_type=ContentType.objects.get_for_model(Contact),
1379
- assigned_object_id=Contact.objects.last().pk,
1388
+ assigned_object_id=Contact.objects.filter(associated_object_metadata__isnull=True).last().pk,
1380
1389
  )
1381
- instance3 = ObjectMetadata.objects.create(
1390
+ instance3 = ObjectMetadata(
1382
1391
  metadata_type=type_contact_team,
1383
1392
  contact=Contact.objects.first(),
1384
1393
  team=None,
1385
1394
  scoped_fields=["email"],
1386
1395
  assigned_object_type=ContentType.objects.get_for_model(Team),
1387
- assigned_object_id=Team.objects.first().pk,
1396
+ assigned_object_id=Team.objects.filter(associated_object_metadata__isnull=True).first().pk,
1388
1397
  )
1389
1398
  with self.assertRaises(ValidationError):
1390
1399
  instance1.validated_save()
@@ -1407,7 +1416,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1407
1416
  value="Some text value",
1408
1417
  scoped_fields=["status", "parent"],
1409
1418
  assigned_object_type=obj_type,
1410
- assigned_object_id=Location.objects.first().pk,
1419
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1411
1420
  )
1412
1421
  obj_metadata.save()
1413
1422
 
@@ -1440,7 +1449,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1440
1449
  value=15,
1441
1450
  scoped_fields=["status", "parent"],
1442
1451
  assigned_object_type=obj_type,
1443
- assigned_object_id=Location.objects.first().pk,
1452
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1444
1453
  )
1445
1454
  obj_metadata.validated_save()
1446
1455
 
@@ -1484,7 +1493,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1484
1493
  value=15.245,
1485
1494
  scoped_fields=["status", "parent"],
1486
1495
  assigned_object_type=obj_type,
1487
- assigned_object_id=Location.objects.first().pk,
1496
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1488
1497
  )
1489
1498
  obj_metadata.validated_save()
1490
1499
 
@@ -1525,7 +1534,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1525
1534
  value=False,
1526
1535
  scoped_fields=["status", "parent"],
1527
1536
  assigned_object_type=obj_type,
1528
- assigned_object_id=Location.objects.first().pk,
1537
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1529
1538
  )
1530
1539
  obj_metadata.validated_save()
1531
1540
 
@@ -1559,7 +1568,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1559
1568
  value="1994-01-01",
1560
1569
  scoped_fields=["status", "parent"],
1561
1570
  assigned_object_type=obj_type,
1562
- assigned_object_id=Location.objects.first().pk,
1571
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1563
1572
  )
1564
1573
  obj_metadata.validated_save()
1565
1574
 
@@ -1596,7 +1605,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1596
1605
  value="2024-06-27T17:58:47-0500",
1597
1606
  scoped_fields=["status", "parent"],
1598
1607
  assigned_object_type=obj_type,
1599
- assigned_object_id=Location.objects.first().pk,
1608
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1600
1609
  )
1601
1610
  obj_metadata.validated_save()
1602
1611
 
@@ -1663,7 +1672,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1663
1672
  value="Option A",
1664
1673
  scoped_fields=["status", "parent"],
1665
1674
  assigned_object_type=obj_type,
1666
- assigned_object_id=Location.objects.first().pk,
1675
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1667
1676
  )
1668
1677
  obj_metadata.validated_save()
1669
1678
 
@@ -1689,7 +1698,7 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1689
1698
  value=["Option A"],
1690
1699
  scoped_fields=["status", "parent"],
1691
1700
  assigned_object_type=obj_type,
1692
- assigned_object_id=Location.objects.first().pk,
1701
+ assigned_object_id=Location.objects.filter(associated_object_metadata__isnull=True).first().pk,
1693
1702
  )
1694
1703
  obj_metadata.validated_save()
1695
1704
 
@@ -1700,20 +1709,22 @@ class ObjectMetadataTest(ModelTestCases.BaseModelTestCase):
1700
1709
  self.assertIn(f"Invalid choice(s) ({invalid_options})", str(context.exception))
1701
1710
 
1702
1711
  def test_no_scoped_fields_overlap(self):
1703
- """Test that overlapping between scoped_fields of ObjectMetadata with the same metadata_type and the same assigned_object is not allowed"""
1712
+ """
1713
+ Test that overlapping scoped_fields of ObjectMetadata with same metadata_type/assigned_object is not allowed.
1714
+ """
1704
1715
  ObjectMetadata.objects.create(
1705
1716
  metadata_type=MetadataType.objects.first(),
1706
1717
  contact=Contact.objects.first(),
1707
1718
  scoped_fields=["host", "mask_length", "type", "role", "status"],
1708
1719
  assigned_object_type=ContentType.objects.get_for_model(IPAddress),
1709
- assigned_object_id=IPAddress.objects.first().pk,
1720
+ assigned_object_id=IPAddress.objects.filter(associated_object_metadata__isnull=True).first().pk,
1710
1721
  )
1711
1722
  instance2 = ObjectMetadata.objects.create(
1712
1723
  metadata_type=MetadataType.objects.first(),
1713
1724
  contact=Contact.objects.first(),
1714
1725
  scoped_fields=[],
1715
1726
  assigned_object_type=ContentType.objects.get_for_model(IPAddress),
1716
- assigned_object_id=IPAddress.objects.first().pk,
1727
+ assigned_object_id=IPAddress.objects.filter(associated_object_metadata__isnull=True).first().pk,
1717
1728
  )
1718
1729
  with self.assertRaises(ValidationError):
1719
1730
  # try scope all fields
@@ -1788,6 +1799,295 @@ class SavedViewTest(ModelTestCases.BaseModelTestCase):
1788
1799
  self.assertEqual(self.ipaddress_global_sv.is_shared, True)
1789
1800
 
1790
1801
 
1802
+ @override_settings(TIME_ZONE="UTC")
1803
+ class ScheduledJobTest(ModelTestCases.BaseModelTestCase):
1804
+ """Tests for the `ScheduledJob` model class."""
1805
+
1806
+ model = ScheduledJob
1807
+
1808
+ def setUp(self):
1809
+ self.user = User.objects.create_user(username="scheduledjobuser")
1810
+ self.job_model = JobModel.objects.get(name="TestPass")
1811
+
1812
+ self.daily_utc_job = ScheduledJob.objects.create(
1813
+ name="Daily UTC Job",
1814
+ task="pass.TestPass",
1815
+ job_model=self.job_model,
1816
+ interval=JobExecutionType.TYPE_DAILY,
1817
+ start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=get_default_timezone()),
1818
+ time_zone=get_default_timezone(),
1819
+ )
1820
+ self.daily_est_job = ScheduledJob.objects.create(
1821
+ name="Daily EST Job",
1822
+ task="pass.TestPass",
1823
+ job_model=self.job_model,
1824
+ interval=JobExecutionType.TYPE_DAILY,
1825
+ start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
1826
+ time_zone=ZoneInfo("America/New_York"),
1827
+ )
1828
+ self.crontab_utc_job = ScheduledJob.create_schedule(
1829
+ job_model=self.job_model,
1830
+ user=self.user,
1831
+ name="Crontab UTC Job",
1832
+ interval=JobExecutionType.TYPE_CUSTOM,
1833
+ crontab="0 17 * * *",
1834
+ )
1835
+ self.crontab_est_job = ScheduledJob.objects.create(
1836
+ name="Crontab EST Job",
1837
+ task="pass.TestPass",
1838
+ job_model=self.job_model,
1839
+ interval=JobExecutionType.TYPE_CUSTOM,
1840
+ start_time=datetime(year=2050, month=1, day=22, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
1841
+ time_zone=ZoneInfo("America/New_York"),
1842
+ crontab="0 17 * * *",
1843
+ )
1844
+ self.one_off_utc_job = ScheduledJob.objects.create(
1845
+ name="One-off UTC Job",
1846
+ task="pass.TestPass",
1847
+ job_model=self.job_model,
1848
+ interval=JobExecutionType.TYPE_FUTURE,
1849
+ start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("UTC")),
1850
+ time_zone=ZoneInfo("UTC"),
1851
+ )
1852
+ self.one_off_est_job = ScheduledJob.create_schedule(
1853
+ job_model=self.job_model,
1854
+ user=self.user,
1855
+ name="One-off EST Job",
1856
+ interval=JobExecutionType.TYPE_FUTURE,
1857
+ start_time=datetime(year=2050, month=1, day=22, hour=0, minute=0, tzinfo=ZoneInfo("America/New_York")),
1858
+ )
1859
+
1860
+ def test_schedule(self):
1861
+ """Test the schedule property."""
1862
+ with self.subTest("Test TYPE_DAILY schedules"):
1863
+ daily_utc_schedule = self.daily_utc_job.schedule
1864
+ daily_est_schedule = self.daily_est_job.schedule
1865
+ self.assertIsInstance(daily_utc_schedule, TzAwareCrontab)
1866
+ self.assertIsInstance(daily_est_schedule, TzAwareCrontab)
1867
+ self.assertNotEqual(daily_utc_schedule, daily_est_schedule)
1868
+ # Crontabs are validated in test_to_cron()
1869
+
1870
+ with self.subTest("Test TYPE_CUSTOM schedules"):
1871
+ crontab_utc_schedule = self.crontab_utc_job.schedule
1872
+ crontab_est_schedule = self.crontab_est_job.schedule
1873
+ self.assertIsInstance(crontab_utc_schedule, TzAwareCrontab)
1874
+ self.assertIsInstance(crontab_est_schedule, TzAwareCrontab)
1875
+ self.assertNotEqual(crontab_utc_schedule, crontab_est_schedule)
1876
+ # Crontabs are validated in test_to_cron()
1877
+
1878
+ with self.subTest("Test TYPE_FUTURE schedules"):
1879
+ # TYPE_FUTURE schedules are one off, not cron tabs:
1880
+ self.assertEqual(self.one_off_utc_job.schedule.clocked_time, self.one_off_utc_job.start_time)
1881
+ self.assertEqual(self.one_off_est_job.schedule.clocked_time, self.one_off_est_job.start_time)
1882
+ self.assertEqual(
1883
+ self.one_off_est_job.schedule.clocked_time - self.one_off_utc_job.schedule.clocked_time,
1884
+ timedelta(hours=5),
1885
+ )
1886
+
1887
+ def test_to_cron(self):
1888
+ """Test the to_cron() method and its interaction with time zone variants."""
1889
+
1890
+ with self.subTest("Test TYPE_DAILY schedule with UTC time zone and UTC schedule time zone"):
1891
+ self.daily_utc_job.refresh_from_db()
1892
+ daily_utc_schedule = self.daily_utc_job.to_cron()
1893
+ self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
1894
+ self.assertEqual(daily_utc_schedule.hour, {17})
1895
+ self.assertEqual(daily_utc_schedule.minute, {0})
1896
+ last_run = datetime(2050, 1, 21, 17, 0, tzinfo=ZoneInfo("UTC"))
1897
+ with time_machine.travel("2050-01-22 16:59 +0000"):
1898
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1899
+ self.assertFalse(is_due)
1900
+ with time_machine.travel("2050-01-22 17:00 +0000"):
1901
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1902
+ self.assertTrue(is_due)
1903
+
1904
+ with self.subTest("Test TYPE_DAILY schedule with UTC time zone and EST schedule time zone"):
1905
+ self.daily_est_job.refresh_from_db()
1906
+ daily_est_schedule = self.daily_est_job.to_cron()
1907
+ self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
1908
+ self.assertEqual(daily_est_schedule.hour, {17})
1909
+ self.assertEqual(daily_est_schedule.minute, {0})
1910
+ last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("UTC"))
1911
+ with time_machine.travel("2050-01-22 21:59 +0000"):
1912
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1913
+ self.assertFalse(is_due)
1914
+ with time_machine.travel("2050-01-22 22:00 +0000"):
1915
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1916
+ self.assertTrue(is_due)
1917
+
1918
+ with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and UTC schedule time zone"):
1919
+ self.crontab_utc_job.refresh_from_db()
1920
+ crontab_utc_schedule = self.crontab_utc_job.to_cron()
1921
+ self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
1922
+ self.assertEqual(crontab_utc_schedule.hour, {17})
1923
+ self.assertEqual(crontab_utc_schedule.minute, {0})
1924
+
1925
+ with self.subTest("Test TYPE_CUSTOM schedule with UTC time zone and EST schedule time zone"):
1926
+ self.crontab_est_job.refresh_from_db()
1927
+ crontab_est_schedule = self.crontab_est_job.to_cron()
1928
+ self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
1929
+ self.assertEqual(crontab_est_schedule.hour, {17})
1930
+ self.assertEqual(crontab_est_schedule.minute, {0})
1931
+
1932
+ with self.subTest("Test TYPE_FUTURE schedules do not map to cron"):
1933
+ with self.assertRaises(ValueError):
1934
+ self.one_off_utc_job.to_cron()
1935
+ with self.assertRaises(ValueError):
1936
+ self.one_off_est_job.to_cron()
1937
+
1938
+ with override_settings(TIME_ZONE="America/New_York"):
1939
+ with self.subTest("Test TYPE_DAILY schedule with EST time zone and UTC schedule time zone"):
1940
+ self.daily_utc_job.refresh_from_db()
1941
+ daily_utc_schedule = self.daily_utc_job.to_cron()
1942
+ self.assertEqual(daily_utc_schedule.tz, ZoneInfo("UTC"))
1943
+ self.assertEqual(daily_utc_schedule.hour, {17})
1944
+ self.assertEqual(daily_utc_schedule.minute, {0})
1945
+ last_run = datetime(2050, 1, 21, 12, 0, tzinfo=ZoneInfo("America/New_York"))
1946
+ with time_machine.travel("2050-01-22 11:59 -0500"):
1947
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1948
+ self.assertFalse(is_due)
1949
+ with time_machine.travel("2050-01-22 12:00 -0500"):
1950
+ is_due, _ = daily_utc_schedule.is_due(last_run_at=last_run)
1951
+ self.assertTrue(is_due)
1952
+
1953
+ with self.subTest("Test TYPE_DAILY schedule with EST time zone and EST schedule time zone"):
1954
+ self.daily_est_job.refresh_from_db()
1955
+ daily_est_schedule = self.daily_est_job.to_cron()
1956
+ self.assertEqual(daily_est_schedule.tz, ZoneInfo("America/New_York"))
1957
+ self.assertEqual(daily_est_schedule.hour, {17})
1958
+ self.assertEqual(daily_est_schedule.minute, {0})
1959
+ last_run = datetime(2050, 1, 21, 22, 0, tzinfo=ZoneInfo("America/New_York"))
1960
+ with time_machine.travel("2050-01-22 16:59 -0500"):
1961
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1962
+ self.assertFalse(is_due)
1963
+ with time_machine.travel("2050-01-22 17:00 -0500"):
1964
+ is_due, _ = daily_est_schedule.is_due(last_run_at=last_run)
1965
+ self.assertTrue(is_due)
1966
+
1967
+ with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and UTC schedule time zone"):
1968
+ self.crontab_utc_job.refresh_from_db()
1969
+ crontab_utc_schedule = self.crontab_utc_job.to_cron()
1970
+ self.assertEqual(crontab_utc_schedule.tz, ZoneInfo("UTC"))
1971
+ self.assertEqual(crontab_utc_schedule.hour, {17})
1972
+ self.assertEqual(crontab_utc_schedule.minute, {0})
1973
+
1974
+ with self.subTest("Test TYPE_CUSTOM schedule with EST time zone and EST schedule time zone"):
1975
+ self.crontab_est_job.refresh_from_db()
1976
+ crontab_est_schedule = self.crontab_est_job.to_cron()
1977
+ self.assertEqual(crontab_est_schedule.tz, ZoneInfo("America/New_York"))
1978
+ self.assertEqual(crontab_est_schedule.hour, {17})
1979
+ self.assertEqual(crontab_est_schedule.minute, {0})
1980
+
1981
+ def test_crontab_dst(self):
1982
+ """Test that TYPE_CUSTOM behavior around DST is as expected."""
1983
+ cronjob = ScheduledJob.objects.create(
1984
+ name="DST Aware Cronjob",
1985
+ task="pass.TestPass",
1986
+ job_model=self.job_model,
1987
+ enabled=False,
1988
+ interval=JobExecutionType.TYPE_CUSTOM,
1989
+ start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
1990
+ crontab="0 17 * * *", # 5 PM local time
1991
+ time_zone=ZoneInfo("America/New_York"),
1992
+ )
1993
+
1994
+ # Before DST takes effect
1995
+ with self.subTest("Test UTC time zone with EST job"):
1996
+ cronjob.refresh_from_db()
1997
+ crontab = cronjob.to_cron()
1998
+ with time_machine.travel("2024-03-09 21:59 +0000"):
1999
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2000
+ self.assertFalse(is_due)
2001
+ with time_machine.travel("2024-03-09 22:00 +0000"):
2002
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2003
+ self.assertTrue(is_due)
2004
+
2005
+ with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
2006
+ cronjob.refresh_from_db()
2007
+ crontab = cronjob.to_cron()
2008
+ with time_machine.travel("2024-03-09 16:59 -0500"):
2009
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2010
+ self.assertFalse(is_due)
2011
+ with time_machine.travel("2024-03-09 17:00 -0500"):
2012
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2013
+ self.assertTrue(is_due)
2014
+
2015
+ # Day that DST takes effect
2016
+ with self.subTest("Test UTC time zone with EDT job"):
2017
+ cronjob.refresh_from_db()
2018
+ crontab = cronjob.to_cron()
2019
+ with time_machine.travel("2024-03-10 20:59 +0000"):
2020
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2021
+ self.assertFalse(is_due)
2022
+ with time_machine.travel("2024-03-10 21:00 +0000"):
2023
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2024
+ self.assertTrue(is_due)
2025
+
2026
+ with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
2027
+ cronjob.refresh_from_db()
2028
+ crontab = cronjob.to_cron()
2029
+ with time_machine.travel("2024-03-10 16:59 -0400"):
2030
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2031
+ self.assertFalse(is_due)
2032
+ with time_machine.travel("2024-03-10 17:00 -0400"):
2033
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2034
+ self.assertTrue(is_due)
2035
+
2036
+ def test_daily_dst(self):
2037
+ """Test the interaction of TYPE_DAILY around DST."""
2038
+ daily = ScheduledJob.objects.create(
2039
+ name="Daily Job",
2040
+ task="pass.TestPass",
2041
+ job_model=self.job_model,
2042
+ enabled=False,
2043
+ interval=JobExecutionType.TYPE_DAILY,
2044
+ start_time=datetime(year=2024, month=1, day=1, hour=17, minute=0, tzinfo=ZoneInfo("America/New_York")),
2045
+ time_zone=ZoneInfo("America/New_York"),
2046
+ )
2047
+
2048
+ # Before DST takes effect
2049
+ with self.subTest("Test UTC time zone with EST job"):
2050
+ daily.refresh_from_db()
2051
+ crontab = daily.to_cron()
2052
+ with time_machine.travel("2024-03-09 21:59 +0000"):
2053
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2054
+ self.assertFalse(is_due)
2055
+ with time_machine.travel("2024-03-09 22:00 +0000"):
2056
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2057
+ self.assertTrue(is_due)
2058
+
2059
+ with self.subTest("Test EST time zone with EST job"), override_settings(TIME_ZONE="America/New_York"):
2060
+ daily.refresh_from_db()
2061
+ crontab = daily.to_cron()
2062
+ with time_machine.travel("2024-03-09 16:59 -0500"):
2063
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2064
+ self.assertFalse(is_due)
2065
+ with time_machine.travel("2024-03-09 17:00 -0500"):
2066
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 8, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2067
+ self.assertTrue(is_due)
2068
+
2069
+ # Day that DST takes effect
2070
+ with self.subTest("Test UTC time zone with EDT job"):
2071
+ daily.refresh_from_db()
2072
+ crontab = daily.to_cron()
2073
+ with time_machine.travel("2024-03-10 20:59 +0000"):
2074
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2075
+ self.assertFalse(is_due)
2076
+ with time_machine.travel("2024-03-10 21:00 +0000"):
2077
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2078
+ self.assertTrue(is_due)
2079
+
2080
+ with self.subTest("Test EDT time zone with EDT job"), override_settings(TIME_ZONE="America/New_York"):
2081
+ daily.refresh_from_db()
2082
+ crontab = daily.to_cron()
2083
+ with time_machine.travel("2024-03-10 16:59 -0400"):
2084
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2085
+ self.assertFalse(is_due)
2086
+ with time_machine.travel("2024-03-10 17:00 -0400"):
2087
+ is_due, _ = crontab.is_due(last_run_at=datetime(2024, 3, 9, 17, 0, tzinfo=ZoneInfo("America/New_York")))
2088
+ self.assertTrue(is_due)
2089
+
2090
+
1791
2091
  class SecretTest(ModelTestCases.BaseModelTestCase):
1792
2092
  """
1793
2093
  Tests for the `Secret` model class.
@@ -1744,16 +1744,17 @@ class ScheduledJobTestCase(
1744
1744
  ScheduledJob.objects.create(
1745
1745
  name="test2",
1746
1746
  task="pass.TestPass",
1747
- interval=JobExecutionType.TYPE_IMMEDIATELY,
1747
+ interval=JobExecutionType.TYPE_DAILY,
1748
1748
  user=user,
1749
1749
  start_time=timezone.now(),
1750
1750
  )
1751
1751
  ScheduledJob.objects.create(
1752
1752
  name="test3",
1753
1753
  task="pass.TestPass",
1754
- interval=JobExecutionType.TYPE_IMMEDIATELY,
1754
+ interval=JobExecutionType.TYPE_CUSTOM,
1755
1755
  user=user,
1756
1756
  start_time=timezone.now(),
1757
+ crontab="15 10 * * *",
1757
1758
  )
1758
1759
 
1759
1760
  def test_only_enabled_is_listed(self):
@@ -2594,7 +2595,7 @@ class JobTestCase(
2594
2595
 
2595
2596
  self.assertInHTML('<option value="uniquequeue" selected>', content)
2596
2597
  self.assertInHTML(
2597
- '<input type="text" name="var" value="456" class="form-control form-control" required placeholder="None" id="id_var">',
2598
+ '<input type="text" name="var" value="456" class="form-control" required placeholder="None" id="id_var">',
2598
2599
  content,
2599
2600
  )
2600
2601
  self.assertInHTML('<input type="hidden" name="_profile" value="True" id="id__profile">', content)
@@ -2994,6 +2995,27 @@ class JobButtonRenderingTestCase(TestCase):
2994
2995
  )
2995
2996
 
2996
2997
 
2998
+ class JobCustomTemplateTestCase(TestCase):
2999
+ @classmethod
3000
+ def setUpTestData(cls):
3001
+ # Job model objects are automatically created during database migrations
3002
+
3003
+ # But we do need to make sure the ones we're testing are flagged appropriately
3004
+ cls.example_job = Job.objects.get(job_class_name="ExampleCustomFormJob")
3005
+ cls.example_job.enabled = True
3006
+ cls.example_job.save()
3007
+
3008
+ cls.run_url = reverse("extras:job_run", kwargs={"pk": cls.example_job.pk})
3009
+
3010
+ def test_rendering_custom_template(self):
3011
+ obj_perm = ObjectPermission(name="Test permission", actions=["view", "run"])
3012
+ obj_perm.save()
3013
+ obj_perm.users.add(self.user)
3014
+ obj_perm.object_types.add(ContentType.objects.get_for_model(Job))
3015
+ with self.assertTemplateUsed("example_app/custom_job_form.html"):
3016
+ self.client.get(self.run_url)
3017
+
3018
+
2997
3019
  # TODO: Convert to StandardTestCases.Views
2998
3020
  class ObjectChangeTestCase(TestCase):
2999
3021
  user_permissions = ("extras.view_objectchange",)
@@ -3029,8 +3051,7 @@ class ObjectChangeTestCase(TestCase):
3029
3051
 
3030
3052
 
3031
3053
  class ObjectMetadataTestCase(
3032
- ViewTestCases.DeleteObjectViewTestCase,
3033
- ViewTestCases.BulkDeleteObjectsViewTestCase,
3054
+ ViewTestCases.GetObjectViewTestCase,
3034
3055
  ViewTestCases.GetObjectChangelogViewTestCase,
3035
3056
  ViewTestCases.ListObjectsViewTestCase,
3036
3057
  ):
nautobot/extras/utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import collections
2
+ import contextlib
2
3
  import hashlib
3
4
  import hmac
4
5
  import logging
@@ -14,6 +15,7 @@ from django.db import transaction
14
15
  from django.db.models import Q
15
16
  from django.template.loader import get_template, TemplateDoesNotExist
16
17
  from django.utils.deconstruct import deconstructible
18
+ import redis.exceptions
17
19
 
18
20
  from nautobot.core.choices import ColorChoices
19
21
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
@@ -109,12 +111,17 @@ class ChangeLoggedModelsQuery(FeaturedQueryMixin):
109
111
  def change_logged_models_queryset():
110
112
  """
111
113
  Cacheable function for cases where we need this queryset many times, such as when saving multiple objects.
114
+
115
+ Cache is cleared by post_migrate signal (nautobot.extras.signals.post_migrate_clear_content_type_caches).
112
116
  """
117
+ queryset = None
113
118
  cache_key = "nautobot.extras.utils.change_logged_models_queryset"
114
- queryset = cache.get(cache_key)
119
+ with contextlib.suppress(redis.exceptions.ConnectionError):
120
+ queryset = cache.get(cache_key)
115
121
  if queryset is None:
116
122
  queryset = ChangeLoggedModelsQuery().as_queryset()
117
- cache.set(cache_key, queryset)
123
+ with contextlib.suppress(redis.exceptions.ConnectionError):
124
+ cache.set(cache_key, queryset)
118
125
  return queryset
119
126
 
120
127
 
@@ -160,7 +167,7 @@ class FeatureQuery:
160
167
  """
161
168
  Given an extras feature, return a iterable of app_label: [models] for content type lookup.
162
169
 
163
- Mis-named, as it returns an iterable of (key, value) (i.e. dict.items()) rather than an actual dict.
170
+ Misnamed, as it returns an iterable of (key, value) (i.e. dict.items()) rather than an actual dict.
164
171
 
165
172
  Raises a KeyError if the given feature doesn't exist.
166
173
  """
@@ -173,12 +180,34 @@ class FeatureQuery:
173
180
 
174
181
  >>> FeatureQuery('statuses').get_choices()
175
182
  [('dcim.device', 13), ('dcim.rack', 34)]
183
+
184
+ Cache is cleared by post_migrate signal (nautobot.extras.signals.post_migrate_clear_content_type_caches).
176
185
  """
177
- return [(f"{ct.app_label}.{ct.model}", ct.pk) for ct in ContentType.objects.filter(self.get_query())]
186
+ choices = None
187
+ cache_key = f"nautobot.extras.utils.FeatureQuery.choices.{self.feature}"
188
+ with contextlib.suppress(redis.exceptions.ConnectionError):
189
+ choices = cache.get(cache_key)
190
+ if choices is None:
191
+ choices = [(f"{ct.app_label}.{ct.model}", ct.pk) for ct in ContentType.objects.filter(self.get_query())]
192
+ with contextlib.suppress(redis.exceptions.ConnectionError):
193
+ cache.set(cache_key, choices)
194
+ return choices
178
195
 
179
196
  def list_subclasses(self):
180
- """Return a list of model classes that declare this feature."""
181
- return [ct.model_class() for ct in ContentType.objects.filter(self.get_query())]
197
+ """
198
+ Return a list of model classes that declare this feature.
199
+
200
+ Cache is cleared by post_migrate signal (nautobot.extras.signals.post_migrate_clear_content_type_caches).
201
+ """
202
+ subclasses = None
203
+ cache_key = f"nautobot.extras.utils.FeatureQuery.subclasses.{self.feature}"
204
+ with contextlib.suppress(redis.exceptions.ConnectionError):
205
+ subclasses = cache.get(cache_key)
206
+ if subclasses is None:
207
+ subclasses = [ct.model_class() for ct in ContentType.objects.filter(self.get_query())]
208
+ with contextlib.suppress(redis.exceptions.ConnectionError):
209
+ cache.set(cache_key, subclasses)
210
+ return subclasses
182
211
 
183
212
 
184
213
  @deconstructible