nautobot 2.3.8__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 (329) hide show
  1. nautobot/apps/tables.py +2 -0
  2. nautobot/core/forms/__init__.py +4 -0
  3. nautobot/core/forms/fields.py +32 -0
  4. nautobot/core/jobs/__init__.py +24 -8
  5. nautobot/core/models/query_functions.py +147 -1
  6. nautobot/core/models/tree_queries.py +8 -0
  7. nautobot/core/settings.py +7 -0
  8. nautobot/core/settings.yaml +10 -0
  9. nautobot/core/signals.py +5 -4
  10. nautobot/core/templates/nautobot_config.py.j2 +4 -0
  11. nautobot/core/tests/test_models_query_functions.py +108 -0
  12. nautobot/dcim/forms.py +30 -27
  13. nautobot/dcim/models/device_components.py +5 -0
  14. nautobot/dcim/tables/devices.py +4 -2
  15. nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
  16. nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
  17. nautobot/dcim/tests/test_models.py +16 -0
  18. nautobot/dcim/views.py +1 -1
  19. nautobot/extras/api/customfields.py +3 -10
  20. nautobot/extras/context_managers.py +28 -9
  21. nautobot/extras/datasources/__init__.py +2 -0
  22. nautobot/extras/datasources/git.py +30 -49
  23. nautobot/extras/datasources/registry.py +2 -2
  24. nautobot/extras/jobs.py +30 -12
  25. nautobot/extras/models/customfields.py +12 -0
  26. nautobot/extras/models/datasources.py +6 -0
  27. nautobot/extras/models/groups.py +47 -33
  28. nautobot/extras/models/jobs.py +1 -1
  29. nautobot/extras/plugins/__init__.py +165 -0
  30. nautobot/extras/signals.py +2 -0
  31. nautobot/extras/tasks.py +88 -69
  32. nautobot/extras/templates/extras/plugin_detail.html +33 -0
  33. nautobot/extras/tests/test_context_managers.py +23 -9
  34. nautobot/extras/tests/test_datasources.py +88 -1
  35. nautobot/extras/tests/test_dynamicgroups.py +12 -0
  36. nautobot/extras/tests/test_plugins.py +94 -0
  37. nautobot/extras/tests/test_webhooks.py +1 -1
  38. nautobot/extras/views.py +3 -1
  39. nautobot/extras/webhooks.py +16 -7
  40. nautobot/project-static/docs/404.html +24 -3
  41. nautobot/project-static/docs/apps/index.html +24 -3
  42. nautobot/project-static/docs/apps/nautobot-apps.html +24 -3
  43. nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js → bundle.83f73b43.min.js} +2 -2
  44. nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js.map → bundle.83f73b43.min.js.map} +3 -3
  45. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css +1 -0
  46. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css.map +1 -0
  47. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +24 -3
  48. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +24 -3
  49. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +24 -3
  50. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +24 -3
  51. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +24 -3
  52. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +24 -3
  53. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +24 -3
  54. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +24 -3
  55. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +24 -3
  56. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +24 -3
  57. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +24 -3
  58. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +24 -3
  59. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +24 -3
  60. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +106 -6
  61. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +24 -3
  62. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +24 -3
  63. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +24 -3
  64. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +138 -3
  65. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +24 -3
  66. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +24 -3
  67. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +24 -3
  68. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +24 -3
  69. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +24 -3
  70. nautobot/project-static/docs/development/apps/api/configuration-view.html +24 -3
  71. nautobot/project-static/docs/development/apps/api/database-backend-config.html +24 -3
  72. nautobot/project-static/docs/development/apps/api/models/django-admin.html +24 -3
  73. nautobot/project-static/docs/development/apps/api/models/global-search.html +24 -3
  74. nautobot/project-static/docs/development/apps/api/models/graphql.html +24 -3
  75. nautobot/project-static/docs/development/apps/api/models/index.html +24 -3
  76. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +24 -3
  77. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +24 -3
  78. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +24 -3
  79. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +24 -3
  80. nautobot/project-static/docs/development/apps/api/platform-features/index.html +24 -3
  81. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +24 -3
  82. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +24 -3
  83. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +24 -3
  84. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +27 -6
  85. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +8823 -0
  86. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +27 -6
  87. nautobot/project-static/docs/development/apps/api/prometheus.html +24 -3
  88. nautobot/project-static/docs/development/apps/api/setup.html +33 -11
  89. nautobot/project-static/docs/development/apps/api/testing.html +24 -3
  90. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +24 -3
  91. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +24 -3
  92. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +24 -3
  93. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +24 -3
  94. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +24 -3
  95. nautobot/project-static/docs/development/apps/api/views/base-template.html +24 -3
  96. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +24 -3
  97. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +24 -3
  98. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +24 -3
  99. nautobot/project-static/docs/development/apps/api/views/index.html +24 -3
  100. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +24 -3
  101. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +24 -3
  102. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +24 -3
  103. nautobot/project-static/docs/development/apps/api/views/notes.html +24 -3
  104. nautobot/project-static/docs/development/apps/api/views/rest-api.html +24 -3
  105. nautobot/project-static/docs/development/apps/api/views/urls.html +24 -3
  106. nautobot/project-static/docs/development/apps/index.html +24 -3
  107. nautobot/project-static/docs/development/apps/migration/code-updates.html +24 -3
  108. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +24 -3
  109. nautobot/project-static/docs/development/apps/migration/from-v1.html +24 -3
  110. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +24 -3
  111. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +24 -3
  112. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +24 -3
  113. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +24 -3
  114. nautobot/project-static/docs/development/apps/porting-from-netbox.html +24 -3
  115. nautobot/project-static/docs/development/core/application-registry.html +24 -3
  116. nautobot/project-static/docs/development/core/best-practices.html +24 -3
  117. nautobot/project-static/docs/development/core/bootstrap-ui.html +24 -3
  118. nautobot/project-static/docs/development/core/caching.html +24 -3
  119. nautobot/project-static/docs/development/core/controllers.html +24 -3
  120. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +24 -3
  121. nautobot/project-static/docs/development/core/generic-views.html +24 -3
  122. nautobot/project-static/docs/development/core/getting-started.html +24 -3
  123. nautobot/project-static/docs/development/core/homepage.html +24 -3
  124. nautobot/project-static/docs/development/core/index.html +24 -3
  125. nautobot/project-static/docs/development/core/model-checklist.html +24 -3
  126. nautobot/project-static/docs/development/core/model-features.html +24 -3
  127. nautobot/project-static/docs/development/core/natural-keys.html +24 -3
  128. nautobot/project-static/docs/development/core/navigation-menu.html +24 -3
  129. nautobot/project-static/docs/development/core/release-checklist.html +24 -3
  130. nautobot/project-static/docs/development/core/role-internals.html +24 -3
  131. nautobot/project-static/docs/development/core/settings.html +24 -3
  132. nautobot/project-static/docs/development/core/style-guide.html +24 -3
  133. nautobot/project-static/docs/development/core/templates.html +24 -3
  134. nautobot/project-static/docs/development/core/testing.html +24 -3
  135. nautobot/project-static/docs/development/core/user-preferences.html +24 -3
  136. nautobot/project-static/docs/development/index.html +24 -3
  137. nautobot/project-static/docs/development/jobs/index.html +24 -3
  138. nautobot/project-static/docs/development/jobs/migration/from-v1.html +24 -3
  139. nautobot/project-static/docs/index.html +24 -3
  140. nautobot/project-static/docs/objects.inv +0 -0
  141. nautobot/project-static/docs/overview/application_stack.html +24 -3
  142. nautobot/project-static/docs/overview/design_philosophy.html +24 -3
  143. nautobot/project-static/docs/release-notes/index.html +24 -3
  144. nautobot/project-static/docs/release-notes/version-1.0.html +24 -3
  145. nautobot/project-static/docs/release-notes/version-1.1.html +24 -3
  146. nautobot/project-static/docs/release-notes/version-1.2.html +24 -3
  147. nautobot/project-static/docs/release-notes/version-1.3.html +24 -3
  148. nautobot/project-static/docs/release-notes/version-1.4.html +24 -3
  149. nautobot/project-static/docs/release-notes/version-1.5.html +24 -3
  150. nautobot/project-static/docs/release-notes/version-1.6.html +24 -3
  151. nautobot/project-static/docs/release-notes/version-2.0.html +24 -3
  152. nautobot/project-static/docs/release-notes/version-2.1.html +24 -3
  153. nautobot/project-static/docs/release-notes/version-2.2.html +24 -3
  154. nautobot/project-static/docs/release-notes/version-2.3.html +430 -114
  155. nautobot/project-static/docs/requirements.txt +1 -1
  156. nautobot/project-static/docs/search/search_index.json +1 -1
  157. nautobot/project-static/docs/sitemap.xml +273 -269
  158. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  159. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +24 -3
  160. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +24 -3
  161. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +24 -3
  162. nautobot/project-static/docs/user-guide/administration/configuration/index.html +24 -3
  163. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +24 -3
  164. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +29 -4
  165. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +24 -3
  166. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +24 -3
  167. nautobot/project-static/docs/user-guide/administration/guides/docker.html +24 -3
  168. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +24 -3
  169. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +24 -3
  170. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +24 -3
  171. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +24 -3
  172. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +24 -3
  173. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +24 -3
  174. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +24 -3
  175. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +24 -3
  176. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +24 -3
  177. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +24 -3
  178. nautobot/project-static/docs/user-guide/administration/installation/index.html +24 -3
  179. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +24 -3
  180. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +24 -3
  181. nautobot/project-static/docs/user-guide/administration/installation/services.html +24 -3
  182. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +24 -3
  183. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +24 -3
  184. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +24 -3
  185. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +24 -3
  186. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +24 -3
  187. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +24 -3
  188. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +24 -3
  189. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +24 -3
  190. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +24 -3
  191. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +24 -3
  192. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +24 -3
  193. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +24 -3
  194. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +24 -3
  195. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +24 -3
  196. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +24 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +24 -3
  198. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +24 -3
  199. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +24 -3
  200. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +24 -3
  201. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +24 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +24 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +24 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +24 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +24 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +24 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +24 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +24 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +24 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +24 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +24 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +24 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +24 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +24 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +24 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +24 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +24 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +24 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +24 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +24 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +24 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +24 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +24 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +24 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +24 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +24 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +24 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +24 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +24 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +24 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +24 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +24 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +24 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +24 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +24 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +24 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +24 -3
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +24 -3
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +24 -3
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +24 -3
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +24 -3
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +24 -3
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +24 -3
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +24 -3
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +24 -3
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +24 -3
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +24 -3
  248. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +24 -3
  249. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +24 -3
  250. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +24 -3
  251. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +24 -3
  252. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +24 -3
  253. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +24 -3
  254. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +24 -3
  255. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +24 -3
  256. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +24 -3
  257. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +24 -3
  258. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +24 -3
  259. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +24 -3
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +24 -3
  261. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +24 -3
  262. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +24 -3
  263. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +24 -3
  264. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +24 -3
  265. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +24 -3
  266. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +24 -3
  267. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +24 -3
  268. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +24 -3
  269. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +24 -3
  270. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +24 -3
  271. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +24 -3
  272. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +24 -3
  273. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +24 -3
  274. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +24 -3
  275. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +24 -3
  276. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +24 -3
  277. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +24 -3
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +24 -3
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +24 -3
  280. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +24 -3
  281. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +24 -3
  282. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +24 -3
  283. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +24 -3
  284. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +24 -3
  285. nautobot/project-static/docs/user-guide/index.html +24 -3
  286. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +24 -3
  287. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +24 -3
  288. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +24 -3
  289. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +24 -3
  290. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +24 -3
  291. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +24 -3
  292. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +24 -3
  293. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +24 -3
  294. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +24 -3
  295. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +24 -3
  296. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +24 -3
  297. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +24 -3
  298. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +24 -3
  299. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +24 -3
  300. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +24 -3
  301. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +24 -3
  302. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +24 -3
  303. nautobot/project-static/docs/user-guide/platform-functionality/note.html +24 -3
  304. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +24 -3
  305. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +24 -3
  306. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +24 -3
  307. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +24 -3
  308. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +24 -3
  309. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +24 -3
  310. nautobot/project-static/docs/user-guide/platform-functionality/role.html +24 -3
  311. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +24 -3
  312. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +24 -3
  313. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +24 -3
  314. nautobot/project-static/docs/user-guide/platform-functionality/status.html +24 -3
  315. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +24 -3
  316. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +24 -3
  317. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +24 -3
  318. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +24 -3
  319. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +24 -3
  320. nautobot/project-static/js/forms.js +3 -5
  321. nautobot/virtualization/tables.py +1 -1
  322. {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/METADATA +3 -3
  323. {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/RECORD +327 -323
  324. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +0 -1
  325. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +0 -1
  326. {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/LICENSE.txt +0 -0
  327. {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/NOTICE +0 -0
  328. {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/WHEEL +0 -0
  329. {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/entry_points.txt +0 -0
@@ -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 %}
@@ -2849,6 +2849,22 @@ class ModuleBayTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
2849
2849
  self.assertIsNone(child_module_bay.parent)
2850
2850
  self.assertIsNone(grandchild_module_bay.parent)
2851
2851
 
2852
+ def test_position_value_auto_population(self):
2853
+ """
2854
+ Assert that the value of the module bay position is auto-populated by its name if position is not provided by the user.
2855
+ """
2856
+
2857
+ module_bay = ModuleBay.objects.create(
2858
+ parent_device=self.device,
2859
+ name="1111",
2860
+ )
2861
+ module_bay.validated_save()
2862
+ self.assertEqual(module_bay.position, module_bay.name)
2863
+ # Test the default value is overriden if the user provides a position value.
2864
+ module_bay.position = "1222"
2865
+ module_bay.validated_save()
2866
+ self.assertEqual(module_bay.position, "1222")
2867
+
2852
2868
 
2853
2869
  class ModuleBayTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, ModelTestCases.BaseModelTestCase):
2854
2870
  model = ModuleBayTemplate
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:
@@ -180,7 +180,7 @@ def web_request_context(
180
180
  Valid choices are in `nautobot.extras.choices.ObjectChangeEventContextChoices`.
181
181
  :param request: Optional web request instance, one will be generated if not supplied
182
182
  """
183
- from nautobot.extras.jobs import enqueue_job_hooks, get_jobs # prevent circular import
183
+ from nautobot.extras.jobs import enqueue_job_hooks # prevent circular import
184
184
 
185
185
  valid_contexts = {
186
186
  ObjectChangeEventContextChoices.CONTEXT_JOB: JobChangeContext,
@@ -203,16 +203,35 @@ def web_request_context(
203
203
  with change_logging(change_context):
204
204
  yield request
205
205
  finally:
206
- jobs_refreshed = False
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
- for object_change in ObjectChange.objects.filter(request_id=change_context.change_id).iterator():
214
+ for object_change in (
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()
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
+
209
224
  if context != ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
210
- # Make sure JobHooks are up to date (once) before calling them
211
- if not jobs_refreshed:
212
- get_jobs(reload=True)
213
- jobs_refreshed = True
214
- enqueue_job_hooks(object_change)
215
- enqueue_webhooks(object_change)
225
+ # Make sure JobHooks are up to date (only once) before calling them
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
216
235
 
217
236
 
218
237
  @contextmanager
@@ -4,6 +4,7 @@ from .git import (
4
4
  ensure_git_repository,
5
5
  get_repo_access_url,
6
6
  git_repository_dry_run,
7
+ refresh_job_code_from_repository,
7
8
  )
8
9
  from .registry import (
9
10
  get_datasource_content_choices,
@@ -20,4 +21,5 @@ __all__ = (
20
21
  "get_repo_access_url",
21
22
  "git_repository_dry_run",
22
23
  "refresh_datasource_content",
24
+ "refresh_job_code_from_repository",
23
25
  )
@@ -96,8 +96,11 @@ def get_repo_access_url(repository_record):
96
96
  obj=repository_record,
97
97
  )
98
98
  except ObjectDoesNotExist:
99
- # No defined secret, fall through to legacy behavior
100
- pass
99
+ logger.warning(
100
+ "HTTP Token not found for secrets group %s associated with repository %s",
101
+ repository_record.secrets_group,
102
+ repository_record,
103
+ )
101
104
  try:
102
105
  user = repository_record.secrets_group.get_secret_value(
103
106
  SecretsGroupAccessTypeChoices.TYPE_HTTP,
@@ -105,8 +108,12 @@ def get_repo_access_url(repository_record):
105
108
  obj=repository_record,
106
109
  )
107
110
  except ObjectDoesNotExist:
108
- # No defined secret, fall through to legacy behavior
109
- pass
111
+ # May not be needed for this repository, so just log as debug rather than warning
112
+ logger.debug(
113
+ "HTTP Username not found for secrets group %s associated with repository %s",
114
+ repository_record.secrets_group,
115
+ repository_record,
116
+ )
110
117
 
111
118
  if token and token not in from_url:
112
119
  # Some git repositories require a user as well as a token.
@@ -147,7 +154,7 @@ def ensure_git_repository(repository_record, logger=None, head=None): # pylint:
147
154
  (bool): Whether any change to the local repo actually occurred.
148
155
  """
149
156
  # We want to check if the repo is already checked out at head. We also want to avoid calling
150
- # get_repo_from_utl_to_path_and_from_branch, because it will cause the URL to be rebuilt causing calls to a secrets
157
+ # get_repo_from_url_to_path_and_from_branch, because it will cause the URL to be rebuilt causing calls to a secrets
151
158
  # backend. As such, if head is None, we can't perform these checks.
152
159
  if head is not None:
153
160
  # If the repo exists and has HEAD already checked out, the repo is present and has the correct branch selected.
@@ -205,7 +212,6 @@ def git_repository_dry_run(repository_record, logger): # pylint: disable=redefi
205
212
  logger.info("%s - `%s`", item.status, item.text)
206
213
  else:
207
214
  logger.info("Repository has no changes")
208
-
209
215
  except Exception as exc:
210
216
  logger.error(str(exc))
211
217
  raise
@@ -435,19 +441,12 @@ def import_config_context(context_data, repository_record, job_result):
435
441
  created = False
436
442
  modified = False
437
443
  save_needed = False
438
- try:
439
- context_record = ConfigContext.objects.get(
440
- name=context_metadata.get("name"),
441
- owner_content_type=git_repository_content_type,
442
- owner_object_id=repository_record.pk,
443
- )
444
- except ConfigContext.DoesNotExist:
445
- context_record = ConfigContext(
446
- name=context_metadata.get("name"),
447
- owner_content_type=git_repository_content_type,
448
- owner_object_id=repository_record.pk,
449
- )
450
- created = True
444
+ context_record, created = ConfigContext.objects.get_or_create(
445
+ name=context_metadata.get("name"),
446
+ owner_content_type=git_repository_content_type,
447
+ owner_object_id=repository_record.pk,
448
+ defaults={"data": {}},
449
+ )
451
450
 
452
451
  for field in ("weight", "description", "is_active"):
453
452
  new_value = context_metadata[field]
@@ -662,20 +661,12 @@ def import_config_context_schema(context_schema_data, repository_record, job_res
662
661
 
663
662
  schema_metadata = context_schema_data["_metadata"]
664
663
 
665
- try:
666
- schema_record = ConfigContextSchema.objects.get(
667
- name=schema_metadata["name"],
668
- owner_content_type=git_repository_content_type,
669
- owner_object_id=repository_record.pk,
670
- )
671
- except ConfigContextSchema.DoesNotExist:
672
- schema_record = ConfigContextSchema(
673
- name=schema_metadata["name"],
674
- owner_content_type=git_repository_content_type,
675
- owner_object_id=repository_record.pk,
676
- data_schema=context_schema_data["data_schema"],
677
- )
678
- created = True
664
+ schema_record, created = ConfigContextSchema.objects.get_or_create(
665
+ name=schema_metadata["name"],
666
+ owner_content_type=git_repository_content_type,
667
+ owner_object_id=repository_record.pk,
668
+ defaults={"data_schema": context_schema_data["data_schema"]},
669
+ )
679
670
 
680
671
  if schema_record.description != schema_metadata.get("description", ""):
681
672
  schema_record.description = schema_metadata.get("description", "")
@@ -868,22 +859,12 @@ def update_git_export_templates(repository_record, job_result):
868
859
  # To reduce noise until the base issue is fixed, we need to explicitly detect object changes:
869
860
  created = False
870
861
  modified = False
871
- try:
872
- template_record = ExportTemplate.objects.get(
873
- content_type=model_content_type,
874
- name=file_name,
875
- owner_content_type=git_repository_content_type,
876
- owner_object_id=repository_record.pk,
877
- )
878
- except ExportTemplate.DoesNotExist:
879
- template_record = ExportTemplate(
880
- content_type=model_content_type,
881
- name=file_name,
882
- owner_content_type=git_repository_content_type,
883
- owner_object_id=repository_record.pk,
884
- )
885
- created = True
886
- modified = True
862
+ template_record, created = ExportTemplate.objects.get_or_create(
863
+ content_type=model_content_type,
864
+ name=file_name,
865
+ owner_content_type=git_repository_content_type,
866
+ owner_object_id=repository_record.pk,
867
+ )
887
868
 
888
869
  if template_record.template_code != template_content:
889
870
  template_record.template_code = template_content
@@ -32,7 +32,7 @@ def refresh_datasource_content(model_name, record, user, job_result, delete=Fals
32
32
  job_result (JobResult): Passed through to the callback functions to use with logging their actions.
33
33
  delete (bool): True if the record is being deleted; False if it is being created/updated.
34
34
  """
35
- job_result.log(f"Refreshing data provided by {record}...", level_choice=LogLevelChoices.LOG_INFO)
35
+ job_result.log(f"Refreshing data provided by {record}...", level_choice=LogLevelChoices.LOG_INFO, obj=record)
36
36
 
37
37
  for entry in get_datasource_contents(model_name):
38
38
  job_result.log(f"Refreshing {entry.name}...", level_choice=LogLevelChoices.LOG_INFO)
@@ -54,4 +54,4 @@ def refresh_datasource_content(model_name, record, user, job_result, delete=Fals
54
54
  raise RuntimeError(msg)
55
55
 
56
56
  # Otherwise, log a friendly info message.
57
- job_result.log(f"Data refresh from {record} complete!", level_choice=LogLevelChoices.LOG_INFO)
57
+ job_result.log(f"Data refresh from {record} complete!", level_choice=LogLevelChoices.LOG_INFO, obj=record)
nautobot/extras/jobs.py CHANGED
@@ -1144,31 +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):
1147
+ def enqueue_job_hooks(object_change, may_reload_jobs=True, jobhook_queryset=None):
1148
1148
  """
1149
- Find job hook(s) assigned to this changed object type + action and enqueue them
1150
- to be processed
1149
+ Find job hook(s) assigned to this changed object type + action and enqueue them to be processed.
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
+
1156
+ Returns:
1157
+ result (tuple[bool, QuerySet]): whether Jobs were reloaded here, and the jobhooks that were considered
1151
1158
  """
1159
+ jobs_reloaded = False
1152
1160
 
1153
1161
  # Job hooks cannot trigger other job hooks
1154
1162
  if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
1155
- return
1163
+ return jobs_reloaded, jobhook_queryset
1156
1164
 
1157
1165
  # Determine whether this type of object supports job hooks
1158
1166
  content_type = object_change.changed_object_type
1159
1167
  if content_type not in change_logged_models_queryset():
1160
- return
1168
+ return jobs_reloaded, jobhook_queryset
1161
1169
 
1162
1170
  # Retrieve any applicable job hooks
1163
- action_flag = {
1164
- ObjectChangeActionChoices.ACTION_CREATE: "type_create",
1165
- ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
1166
- ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
1167
- }[object_change.action]
1168
- 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})
1178
+
1179
+ if not jobhook_queryset: # not .exists() as we *want* to populate the queryset cache
1180
+ return jobs_reloaded, jobhook_queryset
1169
1181
 
1170
1182
  # Enqueue the jobs related to the job_hooks
1171
- for job_hook in job_hooks:
1183
+ if may_reload_jobs:
1184
+ get_jobs(reload=True)
1185
+ jobs_reloaded = True
1186
+
1187
+ for job_hook in jobhook_queryset:
1172
1188
  job_model = job_hook.job
1173
1189
  if not job_model.installed or not job_model.enabled:
1174
1190
  logger.warning(
@@ -1178,3 +1194,5 @@ def enqueue_job_hooks(object_change):
1178
1194
  logger.error("JobHook %s is enabled, but the underlying Job implementation is missing", job_hook)
1179
1195
  else:
1180
1196
  JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)
1197
+
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(
@@ -115,6 +115,12 @@ class GitRepository(PrimaryModel):
115
115
  "provides contents overlapping with this repository."
116
116
  )
117
117
 
118
+ # Changing branch or remote_url invalidates current_head
119
+ if self.present_in_database:
120
+ past = GitRepository.objects.get(id=self.id)
121
+ if self.remote_url != past.remote_url or self.branch != past.branch:
122
+ self.current_head = ""
123
+
118
124
  def get_latest_sync(self):
119
125
  """
120
126
  Return a `JobResult` for the latest sync operation.
@@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
8
8
  from django.core.exceptions import ValidationError
9
9
  from django.core.serializers.json import DjangoJSONEncoder
10
10
  from django.db import models
11
+ from django.db.models.signals import pre_delete
11
12
  from django.utils.functional import cached_property
12
13
  import django_filters
13
14
 
@@ -319,24 +320,18 @@ class DynamicGroup(PrimaryModel):
319
320
  if isinstance(value, models.QuerySet):
320
321
  if value.model != self.model:
321
322
  raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
322
- to_remove = self.members.exclude(pk__in=value.values_list("pk", flat=True))
323
+ to_remove = self.members.only("id").difference(value.only("id"))
323
324
  self._remove_members(to_remove)
324
- to_add = value.exclude(pk__in=self.members.values_list("pk", flat=True))
325
+ to_add = value.only("id").difference(self.members.only("id"))
325
326
  self._add_members(to_add)
326
327
  else:
327
328
  for obj in value:
328
329
  if not isinstance(obj, self.model):
329
330
  raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
330
- to_remove = []
331
- for member in self.members:
332
- if member not in value:
333
- to_remove.append(member)
331
+ existing_members = self.members
332
+ to_remove = [obj for obj in existing_members if obj not in value]
334
333
  self._remove_members(to_remove)
335
- to_add = []
336
- members = self.members
337
- for candidate in value:
338
- if candidate not in members:
339
- to_add.append(candidate)
334
+ to_add = [obj for obj in value if obj not in existing_members]
340
335
  self._add_members(to_add)
341
336
 
342
337
  return self.members
@@ -345,63 +340,81 @@ class DynamicGroup(PrimaryModel):
345
340
  """Add the given list or QuerySet of objects to this staticly defined group."""
346
341
  if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
347
342
  raise ValidationError(f"Group {self} is not staticly defined, adding members directly is not permitted.")
348
- return self._add_members(objects_to_add)
349
-
350
- def _add_members(self, objects_to_add):
351
- """Internal API for adding the given list or QuerySet of objects to the cached/static members of this group."""
352
343
  if isinstance(objects_to_add, models.QuerySet):
353
344
  if objects_to_add.model != self.model:
354
345
  raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
346
+ objects_to_add = objects_to_add.only("id").difference(self.members.only("id"))
355
347
  else:
356
348
  for obj in objects_to_add:
357
349
  if not isinstance(obj, self.model):
358
350
  raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
351
+ existing_members = self.members
352
+ objects_to_add = [obj for obj in objects_to_add if obj not in existing_members]
353
+ return self._add_members(objects_to_add)
359
354
 
355
+ def _add_members(self, objects_to_add):
356
+ """
357
+ Internal API for adding the given list or QuerySet of objects to the cached/static members of this group.
358
+
359
+ Assumes that objects_to_add has already been filtered to exclude any existing member objects.
360
+ """
360
361
  if self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
361
362
  for obj in objects_to_add:
362
363
  # We don't use `.bulk_create()` currently because we want change logging for these creates.
363
364
  # Might be a good future performance improvement though.
364
- StaticGroupAssociation.all_objects.get_or_create(
365
+ StaticGroupAssociation.all_objects.create(
365
366
  dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
366
367
  )
367
368
  else:
368
369
  # Cached/hidden static group associations, so we can use bulk-create to bypass change logging.
369
- existing_members = self.members
370
370
  sgas = [
371
371
  StaticGroupAssociation(
372
372
  dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
373
373
  )
374
374
  for obj in objects_to_add
375
- if obj not in existing_members
376
375
  ]
377
- StaticGroupAssociation.all_objects.bulk_create(sgas)
376
+ StaticGroupAssociation.all_objects.bulk_create(sgas, batch_size=1000)
378
377
 
379
378
  def remove_members(self, objects_to_remove):
380
379
  """Remove the given list or QuerySet of objects from this staticly defined group."""
381
380
  if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
382
381
  raise ValidationError(f"Group {self} is not staticly defined, removing members directly is not permitted.")
383
- return self._remove_members(objects_to_remove)
384
-
385
- def _remove_members(self, objects_to_remove):
386
- """Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
387
382
  if isinstance(objects_to_remove, models.QuerySet):
388
383
  if objects_to_remove.model != self.model:
389
384
  raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
390
- StaticGroupAssociation.all_objects.filter(
391
- dynamic_group=self,
392
- associated_object_type=self.content_type,
393
- associated_object_id__in=objects_to_remove.values_list("pk", flat=True),
394
- ).delete()
395
385
  else:
396
- pks_to_remove = set()
397
386
  for obj in objects_to_remove:
398
387
  if not isinstance(obj, self.model):
399
388
  raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
400
- pks_to_remove.add(obj.pk)
389
+ return self._remove_members(objects_to_remove)
401
390
 
402
- StaticGroupAssociation.all_objects.filter(
403
- dynamic_group=self, associated_object_type=self.content_type, associated_object_id__in=pks_to_remove
404
- ).delete()
391
+ def _remove_members(self, objects_to_remove):
392
+ """Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
393
+ from nautobot.extras.signals import _handle_deleted_object # avoid circular import
394
+
395
+ # For non-static groups, we aren't going to change log the StaticGroupAssociation deletes anyway,
396
+ # so save some performance on signals -- important especially when we're dealing with thousands of records
397
+ if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
398
+ logger.debug("Temporarily disconnecting the _handle_deleted_object signal for performance")
399
+ pre_delete.disconnect(_handle_deleted_object)
400
+ try:
401
+ if isinstance(objects_to_remove, models.QuerySet):
402
+ StaticGroupAssociation.all_objects.filter(
403
+ dynamic_group=self,
404
+ associated_object_type=self.content_type,
405
+ associated_object_id__in=objects_to_remove.values_list("id", flat=True),
406
+ ).delete()
407
+ else:
408
+ pks_to_remove = [obj.id for obj in objects_to_remove]
409
+ StaticGroupAssociation.all_objects.filter(
410
+ dynamic_group=self,
411
+ associated_object_type=self.content_type,
412
+ associated_object_id__in=pks_to_remove,
413
+ ).delete()
414
+ finally:
415
+ if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
416
+ logger.debug("Re-connecting the _handle_deleted_object signal")
417
+ pre_delete.connect(_handle_deleted_object)
405
418
 
406
419
  @property
407
420
  @method_deprecated("Members are now cached in the database via StaticGroupAssociations rather than in Redis.")
@@ -430,6 +443,7 @@ class DynamicGroup(PrimaryModel):
430
443
  else:
431
444
  raise RuntimeError(f"Unknown/invalid group_type {self.group_type}")
432
445
 
446
+ logger.debug("Refreshing members cache for %s", self)
433
447
  self._set_members(members)
434
448
  logger.debug("Refreshed cache for %s, now with %d members", self, self.count)
435
449
 
@@ -754,7 +754,7 @@ class JobResult(BaseModel, CustomFieldModel):
754
754
  grouping="main",
755
755
  ):
756
756
  """
757
- General-purpose API for storing log messages in a JobResult's 'data' field.
757
+ General-purpose API for creating JobLogEntry records associated with a JobResult.
758
758
 
759
759
  message (str): Message to log (an attempt will be made to sanitize sensitive information from this message)
760
760
  obj (object): Object associated with this message, if any