nautobot 2.3.9__py3-none-any.whl → 2.3.10__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 (296) hide show
  1. nautobot/core/models/query_functions.py +147 -1
  2. nautobot/core/tests/test_models_query_functions.py +108 -0
  3. nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
  4. nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
  5. nautobot/dcim/views.py +1 -1
  6. nautobot/extras/api/customfields.py +3 -10
  7. nautobot/extras/context_managers.py +23 -3
  8. nautobot/extras/jobs.py +20 -14
  9. nautobot/extras/models/customfields.py +12 -0
  10. nautobot/extras/signals.py +2 -0
  11. nautobot/extras/tasks.py +88 -69
  12. nautobot/extras/tests/test_context_managers.py +9 -4
  13. nautobot/extras/tests/test_webhooks.py +1 -1
  14. nautobot/extras/webhooks.py +16 -7
  15. nautobot/project-static/docs/404.html +1 -1
  16. nautobot/project-static/docs/apps/index.html +1 -1
  17. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  18. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1 -1
  19. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1 -1
  20. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
  21. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +1 -1
  22. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1 -1
  23. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1 -1
  24. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +1 -1
  25. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1 -1
  26. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +1 -1
  27. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1 -1
  28. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1 -1
  29. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
  30. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1 -1
  31. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +62 -5
  32. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1 -1
  33. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +1 -1
  34. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1 -1
  35. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1 -1
  36. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1 -1
  37. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1 -1
  38. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1 -1
  39. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +1 -1
  40. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1 -1
  41. nautobot/project-static/docs/development/apps/api/configuration-view.html +1 -1
  42. nautobot/project-static/docs/development/apps/api/database-backend-config.html +1 -1
  43. nautobot/project-static/docs/development/apps/api/models/django-admin.html +1 -1
  44. nautobot/project-static/docs/development/apps/api/models/global-search.html +1 -1
  45. nautobot/project-static/docs/development/apps/api/models/graphql.html +1 -1
  46. nautobot/project-static/docs/development/apps/api/models/index.html +1 -1
  47. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +1 -1
  48. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +1 -1
  49. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +1 -1
  50. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +1 -1
  51. nautobot/project-static/docs/development/apps/api/platform-features/index.html +1 -1
  52. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +1 -1
  53. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
  54. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +1 -1
  55. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +1 -1
  56. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +1 -1
  57. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +1 -1
  58. nautobot/project-static/docs/development/apps/api/prometheus.html +1 -1
  59. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  60. nautobot/project-static/docs/development/apps/api/testing.html +1 -1
  61. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +1 -1
  62. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +1 -1
  63. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +1 -1
  64. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +1 -1
  65. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +1 -1
  66. nautobot/project-static/docs/development/apps/api/views/base-template.html +1 -1
  67. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +1 -1
  68. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +1 -1
  69. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +1 -1
  70. nautobot/project-static/docs/development/apps/api/views/index.html +1 -1
  71. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -1
  72. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +1 -1
  73. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +1 -1
  74. nautobot/project-static/docs/development/apps/api/views/notes.html +1 -1
  75. nautobot/project-static/docs/development/apps/api/views/rest-api.html +1 -1
  76. nautobot/project-static/docs/development/apps/api/views/urls.html +1 -1
  77. nautobot/project-static/docs/development/apps/index.html +1 -1
  78. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  79. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
  80. nautobot/project-static/docs/development/apps/migration/from-v1.html +1 -1
  81. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +1 -1
  82. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +1 -1
  83. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +1 -1
  84. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +1 -1
  85. nautobot/project-static/docs/development/apps/porting-from-netbox.html +1 -1
  86. nautobot/project-static/docs/development/core/application-registry.html +1 -1
  87. nautobot/project-static/docs/development/core/best-practices.html +1 -1
  88. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  89. nautobot/project-static/docs/development/core/caching.html +1 -1
  90. nautobot/project-static/docs/development/core/controllers.html +1 -1
  91. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +1 -1
  92. nautobot/project-static/docs/development/core/generic-views.html +1 -1
  93. nautobot/project-static/docs/development/core/getting-started.html +1 -1
  94. nautobot/project-static/docs/development/core/homepage.html +1 -1
  95. nautobot/project-static/docs/development/core/index.html +1 -1
  96. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  97. nautobot/project-static/docs/development/core/model-features.html +1 -1
  98. nautobot/project-static/docs/development/core/natural-keys.html +1 -1
  99. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  100. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  101. nautobot/project-static/docs/development/core/role-internals.html +1 -1
  102. nautobot/project-static/docs/development/core/settings.html +1 -1
  103. nautobot/project-static/docs/development/core/style-guide.html +1 -1
  104. nautobot/project-static/docs/development/core/templates.html +1 -1
  105. nautobot/project-static/docs/development/core/testing.html +1 -1
  106. nautobot/project-static/docs/development/core/user-preferences.html +1 -1
  107. nautobot/project-static/docs/development/index.html +1 -1
  108. nautobot/project-static/docs/development/jobs/index.html +1 -1
  109. nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
  110. nautobot/project-static/docs/index.html +1 -1
  111. nautobot/project-static/docs/overview/application_stack.html +1 -1
  112. nautobot/project-static/docs/overview/design_philosophy.html +1 -1
  113. nautobot/project-static/docs/release-notes/index.html +1 -1
  114. nautobot/project-static/docs/release-notes/version-1.0.html +1 -1
  115. nautobot/project-static/docs/release-notes/version-1.1.html +1 -1
  116. nautobot/project-static/docs/release-notes/version-1.2.html +1 -1
  117. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  118. nautobot/project-static/docs/release-notes/version-1.4.html +1 -1
  119. nautobot/project-static/docs/release-notes/version-1.5.html +1 -1
  120. nautobot/project-static/docs/release-notes/version-1.6.html +1 -1
  121. nautobot/project-static/docs/release-notes/version-2.0.html +1 -1
  122. nautobot/project-static/docs/release-notes/version-2.1.html +1 -1
  123. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  124. nautobot/project-static/docs/release-notes/version-2.3.html +268 -123
  125. nautobot/project-static/docs/requirements.txt +1 -1
  126. nautobot/project-static/docs/search/search_index.json +1 -1
  127. nautobot/project-static/docs/sitemap.xml +270 -270
  128. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  129. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +1 -1
  130. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +1 -1
  131. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +1 -1
  132. nautobot/project-static/docs/user-guide/administration/configuration/index.html +1 -1
  133. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  134. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +1 -1
  135. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +1 -1
  136. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +1 -1
  137. nautobot/project-static/docs/user-guide/administration/guides/docker.html +1 -1
  138. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  139. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +1 -1
  140. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +1 -1
  141. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +1 -1
  142. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +1 -1
  143. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +1 -1
  144. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +1 -1
  145. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  146. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +1 -1
  147. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +1 -1
  148. nautobot/project-static/docs/user-guide/administration/installation/index.html +1 -1
  149. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  150. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -1
  151. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
  152. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +1 -1
  153. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +1 -1
  154. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
  155. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  156. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +1 -1
  157. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +1 -1
  158. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +1 -1
  159. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +1 -1
  160. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +1 -1
  161. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +1 -1
  162. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +1 -1
  163. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  164. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +1 -1
  165. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +1 -1
  166. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +1 -1
  167. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +1 -1
  168. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +1 -1
  169. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +1 -1
  170. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +1 -1
  171. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +1 -1
  172. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +1 -1
  173. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +1 -1
  174. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +1 -1
  175. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +1 -1
  176. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +1 -1
  177. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +1 -1
  178. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +1 -1
  179. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +1 -1
  180. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +1 -1
  181. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +1 -1
  182. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +1 -1
  183. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +1 -1
  184. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +1 -1
  185. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +1 -1
  186. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +1 -1
  187. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +1 -1
  188. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +1 -1
  189. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +1 -1
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +1 -1
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +1 -1
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -1
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +1 -1
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +1 -1
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +1 -1
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +1 -1
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -1
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +1 -1
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +1 -1
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +1 -1
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +1 -1
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +1 -1
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +1 -1
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +1 -1
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +1 -1
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +1 -1
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +1 -1
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +1 -1
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +1 -1
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +1 -1
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +1 -1
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +1 -1
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +1 -1
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +1 -1
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +1 -1
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +1 -1
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +1 -1
  218. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +1 -1
  219. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +1 -1
  220. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +1 -1
  221. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +1 -1
  222. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +1 -1
  223. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +1 -1
  224. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +1 -1
  225. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +1 -1
  226. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +1 -1
  227. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +1 -1
  228. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +1 -1
  229. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +1 -1
  230. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +1 -1
  231. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +1 -1
  232. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +1 -1
  233. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +1 -1
  234. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +1 -1
  235. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +1 -1
  236. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +1 -1
  237. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +1 -1
  238. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +1 -1
  239. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +1 -1
  240. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +1 -1
  241. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +1 -1
  242. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -1
  243. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +1 -1
  244. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +1 -1
  245. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +1 -1
  246. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
  247. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +1 -1
  248. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
  249. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +1 -1
  250. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  251. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +1 -1
  252. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +1 -1
  253. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +1 -1
  254. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +1 -1
  255. nautobot/project-static/docs/user-guide/index.html +1 -1
  256. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +1 -1
  257. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +1 -1
  258. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +1 -1
  259. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +1 -1
  260. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
  261. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +1 -1
  262. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +1 -1
  263. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +1 -1
  264. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +1 -1
  265. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +1 -1
  266. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +1 -1
  267. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +1 -1
  268. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +1 -1
  269. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +1 -1
  270. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +1 -1
  271. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +1 -1
  272. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +1 -1
  273. nautobot/project-static/docs/user-guide/platform-functionality/note.html +1 -1
  274. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +1 -1
  275. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -1
  276. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +1 -1
  277. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +1 -1
  278. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +1 -1
  279. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +1 -1
  280. nautobot/project-static/docs/user-guide/platform-functionality/role.html +1 -1
  281. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +1 -1
  282. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +1 -1
  283. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +1 -1
  284. nautobot/project-static/docs/user-guide/platform-functionality/status.html +1 -1
  285. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +1 -1
  286. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -1
  287. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +1 -1
  288. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +1 -1
  289. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +1 -1
  290. nautobot/project-static/js/forms.js +0 -38
  291. {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/METADATA +2 -2
  292. {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/RECORD +296 -293
  293. {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/LICENSE.txt +0 -0
  294. {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/NOTICE +0 -0
  295. {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/WHEEL +0 -0
  296. {nautobot-2.3.9.dist-info → nautobot-2.3.10.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,7 @@
1
1
  from django.db import NotSupportedError
2
- from django.db.models import Aggregate, Func, JSONField
2
+ from django.db.models import Aggregate, Func, JSONField, Value
3
+ from django.db.models.fields.json import compile_json_path
4
+ from django.db.models.functions import Cast
3
5
 
4
6
 
5
7
  class CollateAsChar(Func):
@@ -26,6 +28,150 @@ class CollateAsChar(Func):
26
28
  return super().as_sql(compiler, connection, function, template, arg_joiner, **extra_context)
27
29
 
28
30
 
31
+ class JSONSet(Func):
32
+ """
33
+ Set or create the value of a single key in a JSONField.
34
+
35
+ Example:
36
+ model.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "cf_key", "new_value"))
37
+
38
+ Limitations:
39
+ - Postgres and MySQL only.
40
+ - Does *not* support nested lookups (`key1__key2`), only a single top-level key.
41
+ - Unlike the referenced Django PR, supports only a single key/value rather than an arbitrary number of them.
42
+
43
+ References:
44
+ - https://code.djangoproject.com/ticket/32519
45
+ - https://github.com/django/django/pull/18489/files
46
+ """
47
+
48
+ function = None
49
+
50
+ def __init__(self, expression, path, value, output_field=None):
51
+ self.path = path
52
+ self.value = value
53
+ super().__init__(expression, output_field=output_field)
54
+
55
+ def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
56
+ """
57
+ Based on https://github.com/django/django/pull/18489/files.
58
+
59
+ Transforms and inserts self.path and self.value appropriately into the expression fields.
60
+ """
61
+ c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
62
+ # Resolve expressions in the JSON update values.
63
+ c.fields = {
64
+ self.path: (
65
+ self.value.resolve_expression(query, allow_joins, reuse, summarize, for_save)
66
+ if hasattr(self.value, "resolve_expression")
67
+ else self.value
68
+ )
69
+ }
70
+ return c
71
+
72
+ def as_sql(self, compiler, connection, function=None, **extra_context):
73
+ """
74
+ MySQL implementation based on https://github.com/django/django/pull/18489/files.
75
+
76
+ Creates a copy of this object with the appropriately transformed self.path and self.value for MySQL JSON_SET().
77
+ """
78
+ if connection.vendor != "mysql":
79
+ raise NotSupportedError(f"JSONSet is not implemented for database {connection.vendor}")
80
+
81
+ copy = self.copy()
82
+ new_source_expressions = copy.get_source_expressions()
83
+
84
+ path = compile_json_path([self.path])
85
+ value = self.value
86
+ if not hasattr(value, "resolve_expression"):
87
+ # Use Value to serialize the value to a string, then Cast to ensure it's treated as JSON.
88
+ value = Cast(Value(value, output_field=self.output_field), output_field=self.output_field)
89
+
90
+ new_source_expressions.extend((Value(path), value))
91
+ copy.set_source_expressions(new_source_expressions)
92
+ return super(JSONSet, copy).as_sql(compiler, connection, function="JSON_SET", **extra_context)
93
+
94
+ def as_postgresql(self, compiler, connection, function=None, **extra_context):
95
+ """
96
+ PostgreSQL implementation based on https://github.com/django/django/pull/18489/files.
97
+
98
+ Creates a copy of this object with appropriately transformed self.path and self.value for Postgres JSONB_SET().
99
+ """
100
+ copy = self.copy()
101
+ new_source_expressions = copy.get_source_expressions()
102
+
103
+ path = self.path
104
+ value = self.value
105
+ if not hasattr(value, "resolve_expression"):
106
+ # We don't need Cast() here because Value with a JSONFIeld is correctly handled as JSONB by Postgres
107
+ value = Value(value, output_field=self.output_field)
108
+ else:
109
+
110
+ class ToJSONB(Func):
111
+ function = "TO_JSONB"
112
+
113
+ value = ToJSONB(value, output_field=self.output_field)
114
+
115
+ new_source_expressions.extend((Value(f"{{{path}}}"), value))
116
+ copy.set_source_expressions(new_source_expressions)
117
+ return super(JSONSet, copy).as_sql(compiler, connection, function="JSONB_SET", **extra_context)
118
+
119
+
120
+ class JSONRemove(Func):
121
+ """
122
+ Unset and remove a single key in a JSONField.
123
+
124
+ Example:
125
+ model.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "cf_key"))
126
+
127
+ Limitations:
128
+ - Postgres and MySQL only.
129
+ - Does *not* support nested lookups (`key1__key2`), only a single top-level key.
130
+ - Unlike the referenced Django PR, supports only a single key, not N keys.
131
+
132
+ References:
133
+ - https://code.djangoproject.com/ticket/32519
134
+ - https://github.com/django/django/pull/18489/files
135
+ """
136
+
137
+ def __init__(self, expression, path):
138
+ self.path = path
139
+ super().__init__(expression)
140
+
141
+ def as_sql(self, compiler, connection, function=None, **extra_context):
142
+ """
143
+ MySQL implementation based on https://github.com/django/django/pull/18489/files.
144
+
145
+ Creates a copy of this object with appropriately transformed self.path for MySQL JSON_REMOVE().
146
+ """
147
+ if connection.vendor != "mysql":
148
+ raise NotSupportedError(f"JSONSet is not implemented for database {connection.vendor}")
149
+
150
+ copy = self.copy()
151
+ new_source_expressions = copy.get_source_expressions()
152
+
153
+ new_source_expressions.append(Value(compile_json_path([self.path])))
154
+
155
+ copy.set_source_expressions(new_source_expressions)
156
+ return super(JSONRemove, copy).as_sql(compiler, connection, function="JSON_REMOVE", **extra_context)
157
+
158
+ def as_postgresql(self, compiler, connection, function=None, **extra_context):
159
+ """
160
+ PostgreSQL implementation based on https://github.com/django/django/pull/18489/files.
161
+
162
+ Creates a copy of this object with appropriately transformed self.path for Postgres `#-` operator.
163
+ """
164
+ copy = self.copy()
165
+ new_source_expressions = copy.get_source_expressions()
166
+
167
+ new_source_expressions.append(Value(f"{{{self.path}}}"))
168
+
169
+ copy.set_source_expressions(new_source_expressions)
170
+ return super(JSONRemove, copy).as_sql(
171
+ compiler, connection, template="%(expressions)s", arg_joiner="#- ", **extra_context
172
+ )
173
+
174
+
29
175
  class JSONBAgg(Aggregate):
30
176
  """
31
177
  Like django.contrib.postgres.aggregates.JSONBAgg, but different.
@@ -0,0 +1,108 @@
1
+ from nautobot.core.models.query_functions import JSONRemove, JSONSet
2
+ from nautobot.core.testing import TestCase
3
+ from nautobot.dcim.models import Manufacturer
4
+
5
+
6
+ class JSONFuncTests(TestCase):
7
+ """Test JSONSet and JSONRemove functionality."""
8
+
9
+ def test_json_set(self):
10
+ # Setting a key/value should efficiently work
11
+ with self.assertNumQueries(1):
12
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", 1))
13
+ for mfr in Manufacturer.objects.all():
14
+ self.assertIn("a", mfr._custom_field_data)
15
+ self.assertEqual(1, mfr._custom_field_data["a"])
16
+
17
+ # Setting a different key/value shouldn't overwrite other keys
18
+ with self.assertNumQueries(1):
19
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "b", "text"))
20
+ for mfr in Manufacturer.objects.all():
21
+ self.assertIn("a", mfr._custom_field_data)
22
+ self.assertEqual(1, mfr._custom_field_data["a"])
23
+ self.assertIn("b", mfr._custom_field_data)
24
+ self.assertEqual("text", mfr._custom_field_data["b"])
25
+
26
+ # Setting a key/value again should overwrite that value only
27
+ with self.assertNumQueries(1):
28
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "b", "more text"))
29
+ for mfr in Manufacturer.objects.all():
30
+ self.assertIn("a", mfr._custom_field_data)
31
+ self.assertEqual(1, mfr._custom_field_data["a"])
32
+ self.assertIn("b", mfr._custom_field_data)
33
+ self.assertEqual("more text", mfr._custom_field_data["b"])
34
+
35
+ # A filtered query should be updatable
36
+ with self.assertNumQueries(1):
37
+ Manufacturer.objects.filter(name__istartswith="a").update(
38
+ _custom_field_data=JSONSet("_custom_field_data", "a", None)
39
+ )
40
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
41
+ self.assertIn("a", mfr._custom_field_data)
42
+ self.assertEqual(None, mfr._custom_field_data["a"])
43
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
44
+ self.assertIn("a", mfr._custom_field_data)
45
+ self.assertEqual(1, mfr._custom_field_data["a"])
46
+ for mfr in Manufacturer.objects.all():
47
+ self.assertIn("b", mfr._custom_field_data)
48
+ self.assertEqual("more text", mfr._custom_field_data["b"])
49
+
50
+ # Setting a value doesn't require all existing values to be homogeneous
51
+ with self.assertNumQueries(1):
52
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", "hello"))
53
+ for mfr in Manufacturer.objects.all():
54
+ self.assertIn("a", mfr._custom_field_data)
55
+ self.assertEqual("hello", mfr._custom_field_data["a"])
56
+ self.assertIn("b", mfr._custom_field_data)
57
+ self.assertEqual("more text", mfr._custom_field_data["b"])
58
+
59
+ def test_json_remove(self):
60
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", 1))
61
+ Manufacturer.objects.filter(name__istartswith="a").update(
62
+ _custom_field_data=JSONSet("_custom_field_data", "b", "hello")
63
+ )
64
+ Manufacturer.objects.exclude(name__istartswith="a").update(
65
+ _custom_field_data=JSONSet("_custom_field_data", "b", "world")
66
+ )
67
+
68
+ # Should be able to clear all values for a key without impacting other keys
69
+ with self.assertNumQueries(1):
70
+ Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "a"))
71
+ for mfr in Manufacturer.objects.all():
72
+ self.assertNotIn("a", mfr._custom_field_data)
73
+ self.assertIn("b", mfr._custom_field_data)
74
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
75
+ self.assertEqual("hello", mfr._custom_field_data["b"])
76
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
77
+ self.assertEqual("world", mfr._custom_field_data["b"])
78
+
79
+ # Clearing a value that doesn't exist should be safe
80
+ with self.assertNumQueries(1):
81
+ Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "a"))
82
+ for mfr in Manufacturer.objects.all():
83
+ self.assertNotIn("a", mfr._custom_field_data)
84
+ self.assertIn("b", mfr._custom_field_data)
85
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
86
+ self.assertEqual("hello", mfr._custom_field_data["b"])
87
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
88
+ self.assertEqual("world", mfr._custom_field_data["b"])
89
+
90
+ # Subsets should be updateable
91
+ with self.assertNumQueries(1):
92
+ Manufacturer.objects.filter(name__istartswith="a").update(
93
+ _custom_field_data=JSONRemove("_custom_field_data", "b")
94
+ )
95
+ for mfr in Manufacturer.objects.all():
96
+ self.assertNotIn("a", mfr._custom_field_data)
97
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
98
+ self.assertNotIn("b", mfr._custom_field_data)
99
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
100
+ self.assertIn("b", mfr._custom_field_data)
101
+ self.assertEqual("world", mfr._custom_field_data["b"])
102
+
103
+ # Non-homogeneous data should be updatable
104
+ with self.assertNumQueries(1):
105
+ Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "b"))
106
+ for mfr in Manufacturer.objects.all():
107
+ self.assertNotIn("a", mfr._custom_field_data)
108
+ self.assertNotIn("b", mfr._custom_field_data)
@@ -0,0 +1,39 @@
1
+ {% extends 'dcim/device_component_add.html' %}
2
+
3
+ {% block javascript %}
4
+ {{ block.super }}
5
+ <script>
6
+ document.addEventListener('DOMContentLoaded', function() {
7
+ var position_field = document.getElementById('id_position_pattern');
8
+ var source_arr = position_field.getAttribute('source').split(" ");
9
+ var length = position_field.getAttribute('maxlength');
10
+ position_field.setAttribute('_changed', Boolean(position_field.value))
11
+ position_field.addEventListener('change', function() {
12
+ position_field.setAttribute('_changed', Boolean(position_field.value))
13
+ });
14
+ function repopulate() {
15
+ let str = "";
16
+ for (source_str of source_arr) {
17
+ if (str != "") {
18
+ str += " ";
19
+ }
20
+ let source_id = 'id_' + source_str;
21
+ let source = document.getElementById(source_id)
22
+ str += source.value;
23
+ }
24
+ position_field.value = str.slice(0, length ? length : 255);
25
+ };
26
+ for (source_str of source_arr) {
27
+ let source_id = 'id_' + source_str;
28
+ let source = document.getElementById(source_id);
29
+ source.addEventListener('keyup', function() {
30
+ if (position_field && position_field.getAttribute('_changed')=="false") {
31
+ repopulate();
32
+ }
33
+ });
34
+ }
35
+ document.getElementsByClassName('reslugify')[0].addEventListener('click', repopulate);
36
+ document.getElementsByClassName('reslugify')[0].setAttribute('data-original-title', "Regenerate position");
37
+ });
38
+ </script>
39
+ {% endblock javascript %}
@@ -0,0 +1,39 @@
1
+ {% extends 'generic/object_create.html' %}
2
+
3
+ {% block javascript %}
4
+ {{ block.super }}
5
+ <script>
6
+ document.addEventListener('DOMContentLoaded', function() {
7
+ var position_field = document.getElementById('id_position');
8
+ var source_arr = position_field.getAttribute('source').split(" ");
9
+ var length = position_field.getAttribute('maxlength');
10
+ position_field.setAttribute('_changed', Boolean(position_field.value))
11
+ position_field.addEventListener('change', function() {
12
+ position_field.setAttribute('_changed', Boolean(position_field.value))
13
+ });
14
+ function repopulate() {
15
+ let str = "";
16
+ for (source_str of source_arr) {
17
+ if (str != "") {
18
+ str += " ";
19
+ }
20
+ let source_id = 'id_' + source_str;
21
+ let source = document.getElementById(source_id)
22
+ str += source.value;
23
+ }
24
+ position_field.value = str.slice(0, length ? length : 255);
25
+ };
26
+ for (source_str of source_arr) {
27
+ let source_id = 'id_' + source_str;
28
+ let source = document.getElementById(source_id);
29
+ source.addEventListener('keyup', function() {
30
+ if (position_field && position_field.getAttribute('_changed')=="false") {
31
+ repopulate();
32
+ }
33
+ });
34
+ }
35
+ document.getElementsByClassName('reslugify')[0].addEventListener('click', repopulate);
36
+ document.getElementsByClassName('reslugify')[0].setAttribute('data-original-title', "Regenerate position");
37
+ });
38
+ </script>
39
+ {% endblock javascript %}
nautobot/dcim/views.py CHANGED
@@ -3241,7 +3241,7 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3241
3241
  model_form_class = forms.ModuleBayForm
3242
3242
  serializer_class = serializers.ModuleBaySerializer
3243
3243
  table_class = tables.ModuleBayTable
3244
- create_template_name = "dcim/device_component_add.html"
3244
+ create_template_name = "dcim/modulebay_create.html"
3245
3245
 
3246
3246
  def get_extra_context(self, request, instance):
3247
3247
  if instance:
@@ -40,15 +40,7 @@ class CustomFieldDefaultValues:
40
40
  class CustomFieldsDataField(Field):
41
41
  @property
42
42
  def custom_field_keys(self):
43
- """
44
- Cache CustomField keys assigned to this model to avoid redundant database queries
45
- """
46
- if not hasattr(self, "_custom_field_keys"):
47
- content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
48
- self._custom_field_keys = CustomField.objects.filter(content_types=content_type).values_list(
49
- "key", flat=True
50
- )
51
- return self._custom_field_keys
43
+ return CustomField.objects.keys_for_model(self.parent.Meta.model)
52
44
 
53
45
  def to_representation(self, obj):
54
46
  return {key: obj.get(key) for key in self.custom_field_keys}
@@ -58,7 +50,8 @@ class CustomFieldsDataField(Field):
58
50
 
59
51
  # Discard any entries in data that do not align with actual CustomFields - this matches the REST API behavior
60
52
  # for top-level serializer fields that do not exist or are not writable
61
- data = {key: value for key, value in data.items() if key in self.custom_field_keys}
53
+ custom_field_keys = self.custom_field_keys
54
+ data = {key: value for key, value in data.items() if key in custom_field_keys}
62
55
 
63
56
  # If updating an existing instance, start with existing _custom_field_data
64
57
  if self.parent.instance:
@@ -204,14 +204,34 @@ def web_request_context(
204
204
  yield request
205
205
  finally:
206
206
  jobs_reloaded = False
207
+ # In bulk operations, we are performing the same action (create/update/delete) on the same content-type.
208
+ # Save some repeated database queries by reusing the same evaluated querysets where applicable:
209
+ jobhook_queryset = None
210
+ webhook_queryset = None
211
+ last_action = None
212
+ last_content_type = None
207
213
  # enqueue jobhooks and webhooks, use change_context.change_id in case change_id was not supplied
208
214
  for object_change in (
209
- ObjectChange.objects.filter(request_id=change_context.change_id).order_by("time").iterator()
215
+ ObjectChange.objects.select_related("changed_object_type", "user")
216
+ .filter(request_id=change_context.change_id)
217
+ .order_by("time") # default ordering is -time but we want oldest first not newest first
218
+ .iterator()
210
219
  ):
220
+ if object_change.action != last_action or object_change.changed_object_type != last_content_type:
221
+ jobhook_queryset = None
222
+ webhook_queryset = None
223
+
211
224
  if context != ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
212
225
  # Make sure JobHooks are up to date (only once) before calling them
213
- jobs_reloaded |= enqueue_job_hooks(object_change, may_reload_jobs=(not jobs_reloaded))
214
- enqueue_webhooks(object_change)
226
+ did_reload_jobs, jobhook_queryset = enqueue_job_hooks(
227
+ object_change, may_reload_jobs=(not jobs_reloaded), jobhook_queryset=jobhook_queryset
228
+ )
229
+ if did_reload_jobs:
230
+ jobs_reloaded = True
231
+
232
+ webhook_queryset = enqueue_webhooks(object_change, webhook_queryset=webhook_queryset)
233
+ last_action = object_change.action
234
+ last_content_type = object_change.changed_object_type
215
235
 
216
236
 
217
237
  @contextmanager
nautobot/extras/jobs.py CHANGED
@@ -1144,41 +1144,47 @@ def run_job(self, job_class_path, *args, **kwargs):
1144
1144
  raise
1145
1145
 
1146
1146
 
1147
- def enqueue_job_hooks(object_change, may_reload_jobs=True):
1147
+ def enqueue_job_hooks(object_change, may_reload_jobs=True, jobhook_queryset=None):
1148
1148
  """
1149
1149
  Find job hook(s) assigned to this changed object type + action and enqueue them to be processed.
1150
1150
 
1151
+ Args:
1152
+ object_change (ObjectChange): The change that may trigger JobHooks to execute.
1153
+ may_reload_jobs (bool): Whether to reload JobHook source code from disk to guarantee up-to-date code.
1154
+ jobhook_queryset (QuerySet): Previously retrieved set of JobHooks to potentially enqueue
1155
+
1151
1156
  Returns:
1152
- jobs_reloaded (bool): whether Jobs were reloaded to make this happen
1157
+ result (tuple[bool, QuerySet]): whether Jobs were reloaded here, and the jobhooks that were considered
1153
1158
  """
1154
1159
  jobs_reloaded = False
1155
1160
 
1156
1161
  # Job hooks cannot trigger other job hooks
1157
1162
  if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
1158
- return jobs_reloaded
1163
+ return jobs_reloaded, jobhook_queryset
1159
1164
 
1160
1165
  # Determine whether this type of object supports job hooks
1161
1166
  content_type = object_change.changed_object_type
1162
1167
  if content_type not in change_logged_models_queryset():
1163
- return jobs_reloaded
1168
+ return jobs_reloaded, jobhook_queryset
1164
1169
 
1165
1170
  # Retrieve any applicable job hooks
1166
- action_flag = {
1167
- ObjectChangeActionChoices.ACTION_CREATE: "type_create",
1168
- ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
1169
- ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
1170
- }[object_change.action]
1171
- job_hooks = JobHook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
1171
+ if jobhook_queryset is None:
1172
+ action_flag = {
1173
+ ObjectChangeActionChoices.ACTION_CREATE: "type_create",
1174
+ ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
1175
+ ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
1176
+ }[object_change.action]
1177
+ jobhook_queryset = JobHook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
1172
1178
 
1173
- if not job_hooks.exists():
1174
- return jobs_reloaded
1179
+ if not jobhook_queryset: # not .exists() as we *want* to populate the queryset cache
1180
+ return jobs_reloaded, jobhook_queryset
1175
1181
 
1176
1182
  # Enqueue the jobs related to the job_hooks
1177
1183
  if may_reload_jobs:
1178
1184
  get_jobs(reload=True)
1179
1185
  jobs_reloaded = True
1180
1186
 
1181
- for job_hook in job_hooks:
1187
+ for job_hook in jobhook_queryset:
1182
1188
  job_model = job_hook.job
1183
1189
  if not job_model.installed or not job_model.enabled:
1184
1190
  logger.warning(
@@ -1189,4 +1195,4 @@ def enqueue_job_hooks(object_change, may_reload_jobs=True):
1189
1195
  else:
1190
1196
  JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)
1191
1197
 
1192
- return jobs_reloaded
1198
+ return jobs_reloaded, jobhook_queryset
@@ -391,6 +391,18 @@ class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
391
391
 
392
392
  get_for_model.cache_key_prefix = "nautobot.extras.customfield.get_for_model"
393
393
 
394
+ def keys_for_model(self, model):
395
+ """Return list of all keys for CustomFields assigned to the given model."""
396
+ concrete_model = model._meta.concrete_model
397
+ cache_key = f"{self.keys_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}"
398
+ keys = cache.get(cache_key)
399
+ if keys is None:
400
+ keys = list(self.get_for_model(model).values_list("key", flat=True))
401
+ cache.set(cache_key, keys)
402
+ return keys
403
+
404
+ keys_for_model.cache_key_prefix = "nautobot.extras.customfield.keys_for_model"
405
+
394
406
 
395
407
  @extras_features("webhooks")
396
408
  class CustomField(
@@ -91,6 +91,8 @@ def invalidate_models_cache(sender, **kwargs):
91
91
  with contextlib.suppress(redis.exceptions.ConnectionError):
92
92
  # TODO: *maybe* target more narrowly, e.g. only clear the cache for specific related content-types?
93
93
  cache.delete_pattern(f"{manager.get_for_model.cache_key_prefix}.*")
94
+ if hasattr(manager, "keys_for_model"):
95
+ cache.delete_pattern(f"{manager.keys_for_model.cache_key_prefix}.*")
94
96
 
95
97
 
96
98
  @receiver(post_save, sender=Relationship)