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
@@ -94,6 +94,7 @@ class NautobotAppConfig(NautobotConfig):
94
94
  metrics = "metrics.metrics"
95
95
  menu_items = "navigation.menu_items"
96
96
  secrets_providers = "secrets.secrets_providers"
97
+ table_extensions = "table_extensions.table_extensions"
97
98
  template_extensions = "template_content.template_extensions"
98
99
  override_views = "views.override_views"
99
100
 
@@ -211,6 +212,9 @@ class NautobotAppConfig(NautobotConfig):
211
212
  )
212
213
  register_override_views(override_views, self.name)
213
214
 
215
+ # Register tables extensions (if any).
216
+ self._register_table_extensions()
217
+
214
218
  @classmethod
215
219
  def validate(cls, user_config, nautobot_version):
216
220
  """Validate the user_config for baseline correctness."""
@@ -262,6 +266,13 @@ class NautobotAppConfig(NautobotConfig):
262
266
  if setting not in user_config and setting not in cls.constance_config:
263
267
  user_config[setting] = value
264
268
 
269
+ def _register_table_extensions(self):
270
+ """Register tables extensions (if any)."""
271
+ table_extensions = import_object(f"{self.__module__}.{self.table_extensions}")
272
+ if table_extensions is not None:
273
+ register_table_extensions(table_extensions, self.name)
274
+ self.features["table_extensions"] = get_table_extension_features(table_extensions)
275
+
265
276
 
266
277
  @class_deprecated_in_favor_of(NautobotAppConfig)
267
278
  class PluginConfig(NautobotAppConfig):
@@ -491,6 +502,160 @@ def register_filter_extensions(filter_extensions, plugin_name):
491
502
  )
492
503
 
493
504
 
505
+ #
506
+ # Table Extensions
507
+ #
508
+
509
+
510
+ class TableExtension:
511
+ """Template class for extending Tables.
512
+
513
+ An app can override the default columns for a table by either:
514
+ - Extending the original default columns to include custom columns.
515
+ - add_to_default_columns = ("my_app_name_new_column",)
516
+ - Removing native columns from the default columns.
517
+ - remove_from_default_columns = ("tenant",)
518
+ """
519
+
520
+ model = None
521
+ table_columns = {}
522
+ add_to_default_columns = ()
523
+ remove_from_default_columns = ()
524
+
525
+ @classmethod
526
+ def alter_queryset(cls, queryset):
527
+ """Alter the View class QuerySet.
528
+
529
+ This is a good place to add `prefetch_related` to the view queryset.
530
+ example:
531
+ return queryset.prefetch_related("my_model_set")
532
+ """
533
+ return queryset
534
+
535
+ @classmethod
536
+ def _get_table_columns_registrations(cls):
537
+ """Return a list of register labels fro each column."""
538
+ if not cls.table_columns:
539
+ return []
540
+ return [f"{cls.model} -> {column_name}" for column_name in cls.table_columns]
541
+
542
+ @classmethod
543
+ def _get_add_to_default_columns_registrations(cls):
544
+ """Return a list of register labels for each column added to defaults."""
545
+ if not cls.add_to_default_columns:
546
+ return []
547
+ return [f"{cls.model} -> {cls.add_to_default_columns}"]
548
+
549
+ @classmethod
550
+ def _get_remove_from_default_columns_registrations(cls):
551
+ """Return a list of register labels for each column removed from defaults."""
552
+ if not cls.remove_from_default_columns:
553
+ return []
554
+ return [f"{cls.model} -> {cls.remove_from_default_columns}"]
555
+
556
+
557
+ def get_table_extension_features(table_extensions):
558
+ """Return a dictionary of TableExtension features for the App detail view."""
559
+ return {
560
+ "columns": [
561
+ label
562
+ for table_extension in table_extensions
563
+ for label in table_extension._get_table_columns_registrations()
564
+ ],
565
+ "add_to_default_columns": [
566
+ label
567
+ for table_extension in table_extensions
568
+ for label in table_extension._get_add_to_default_columns_registrations()
569
+ ],
570
+ "remove_from_default_columns": [
571
+ label
572
+ for table_extension in table_extensions
573
+ for label in table_extension._get_remove_from_default_columns_registrations()
574
+ ],
575
+ }
576
+
577
+
578
+ def register_table_extensions(table_extensions, app_name):
579
+ """Register a list of TableExtension classes."""
580
+ for table_extension in table_extensions:
581
+ _validate_is_subclass_of_table_extension(table_extension)
582
+ _add_columns_into_model_table(table_extension, app_name)
583
+ _modify_default_table_columns(table_extension, app_name)
584
+ _alter_table_view_queryset(table_extension, app_name)
585
+
586
+
587
+ def _add_columns_into_model_table(table_extension, app_name):
588
+ """Inject each new column into the Model Table."""
589
+ from nautobot.core.utils.lookup import get_table_for_model
590
+
591
+ if not isinstance(table_extension.table_columns, dict):
592
+ error = f"{app_name} TableExtension: 'table_columns' attribute must be of type 'dict'."
593
+ logger.error(error)
594
+ return
595
+
596
+ table = get_table_for_model(table_extension.model)
597
+ for name, column in table_extension.table_columns.items():
598
+ _validate_table_column_name_is_prefixed_with_app_name(name, app_name)
599
+ _add_column_to_table_base_columns(table, name, column, app_name)
600
+
601
+
602
+ def _add_column_to_table_base_columns(table, column_name, column, app_name):
603
+ """Attach a column to an existing table."""
604
+ import django_tables2
605
+
606
+ if not isinstance(column, django_tables2.Column):
607
+ raise TypeError(f"Custom column `{column_name}` is not an instance of django_tables2.Column.")
608
+
609
+ if column_name in table.base_columns:
610
+ logger.error(
611
+ f"{app_name}: There was a name conflict with existing table column `{column_name}`, the custom column was ignored."
612
+ )
613
+ else:
614
+ table.base_columns[column_name] = column
615
+
616
+
617
+ def _alter_table_view_queryset(table_extension, app_name):
618
+ """Replace the model view queryset with an optimized queryset from the app."""
619
+ from nautobot.core.utils.lookup import get_view_for_model
620
+
621
+ # TODO: Investigate if there is a more targeted way to patch only the list view queryset
622
+ # when targeting a subclass of `NautobotUIViewSet`.
623
+ view = get_view_for_model(table_extension.model, view_type="List")
624
+ view.queryset = table_extension.alter_queryset(view.queryset)
625
+
626
+
627
+ def _modify_default_table_columns(table_extension, app_name):
628
+ """Add or remove columns from the table default columns."""
629
+ from nautobot.core.utils.lookup import get_table_for_model
630
+
631
+ table = get_table_for_model(table_extension.model)
632
+ message = (
633
+ f"{app_name}: Cannot {{action}} column `{{column_name}}` {{preposition}} the default columns for `{table}`."
634
+ )
635
+
636
+ for column_name in table_extension.add_to_default_columns:
637
+ if column_name in table.base_columns:
638
+ table.Meta.default_columns = (*table.Meta.default_columns, column_name)
639
+ else:
640
+ logger.debug(message.format(action="add", column_name=column_name, preposition="to"))
641
+
642
+ for column_name in table_extension.remove_from_default_columns:
643
+ if column_name in table.Meta.default_columns:
644
+ table.Meta.default_columns = tuple(name for name in table.Meta.default_columns if name != column_name)
645
+ else:
646
+ logger.debug(message.format(action="remove", column_name=column_name, preposition="from"))
647
+
648
+
649
+ def _validate_is_subclass_of_table_extension(table_extension):
650
+ if not issubclass(table_extension, TableExtension):
651
+ raise TypeError(f"{table_extension} is not a subclass of nautobot.apps.filters.TableExtension!")
652
+
653
+
654
+ def _validate_table_column_name_is_prefixed_with_app_name(name, app_name):
655
+ if not name.startswith(f"{app_name}_"):
656
+ raise ValueError(f"Attempted to create a custom table column `{name}` that did not start with `{app_name}`")
657
+
658
+
494
659
  #
495
660
  # Navigation menu links
496
661
  #
@@ -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)
nautobot/extras/tasks.py CHANGED
@@ -2,18 +2,42 @@ from logging import getLogger
2
2
 
3
3
  from django.conf import settings
4
4
  from django.contrib.contenttypes.models import ContentType
5
- from django.db import transaction
6
5
  from jinja2.exceptions import TemplateError
7
6
  import requests
8
7
 
9
8
  from nautobot.core.celery import nautobot_task
9
+ from nautobot.core.models.query_functions import JSONRemove, JSONSet
10
10
  from nautobot.extras.choices import CustomFieldTypeChoices, ObjectChangeActionChoices
11
11
  from nautobot.extras.utils import generate_signature
12
12
 
13
13
  logger = getLogger("nautobot.extras.tasks")
14
14
 
15
15
 
16
- @nautobot_task
16
+ def _generate_bulk_object_changes(context, queryset, task_logger):
17
+ # Circular import
18
+ from nautobot.extras.context_managers import (
19
+ change_logging,
20
+ ChangeContext,
21
+ deferred_change_logging_for_bulk_operation,
22
+ )
23
+ from nautobot.extras.signals import _handle_changed_object
24
+
25
+ task_logger.info("Creating deferred ObjectChange records for bulk operation...")
26
+
27
+ # Note: we use change_logging() here instead of web_request_context() because we don't want these change records to
28
+ # trigger jobhooks and webhooks.
29
+ # TODO: this could be made much faster if we ensure the queryset has appropriate select_related/prefetch_related?
30
+ change_context = ChangeContext(**context)
31
+ i = 0
32
+ with change_logging(change_context):
33
+ with deferred_change_logging_for_bulk_operation():
34
+ for i, instance in enumerate(queryset.iterator(), start=1):
35
+ _handle_changed_object(queryset.model, instance, created=False)
36
+
37
+ task_logger.info("Created %d ObjectChange records", i)
38
+
39
+
40
+ @nautobot_task(soft_time_limit=1800, time_limit=2000)
17
41
  def update_custom_field_choice_data(field_id, old_value, new_value, change_context=None):
18
42
  """
19
43
  Update the values for a custom field choice used in objects' _custom_field_data for the given field.
@@ -22,47 +46,48 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
22
46
  field_id (uuid4): The PK of the custom field to which this choice value relates
23
47
  old_value (str): The existing value of the choice
24
48
  new_value (str): The value which will be used as replacement
49
+ change_context (dict): Optional dict representation of change context for ObjectChange creation
25
50
  """
26
51
  # Circular Import
27
52
  from nautobot.extras.context_managers import web_request_context
28
53
  from nautobot.extras.models import CustomField
29
54
 
55
+ task_logger = getLogger("celery.task.update_custom_field_choice_data")
56
+
30
57
  try:
31
58
  field = CustomField.objects.get(pk=field_id)
32
59
  except CustomField.DoesNotExist:
33
- logger.error(f"Custom field with ID {field_id} not found, failing to act on choice data.")
34
- return False
60
+ task_logger.error("Custom field with ID %s not found, failing to act on choice data.", field_id)
61
+ raise
35
62
 
36
63
  if field.type == CustomFieldTypeChoices.TYPE_SELECT:
37
64
  # Loop through all field content types and search for values to update
38
65
  for ct in field.content_types.all():
39
66
  model = ct.model_class()
67
+ queryset = model.objects.filter(**{f"_custom_field_data__{field.key}": old_value})
40
68
  if change_context is not None:
41
- with web_request_context(
42
- user=change_context.get("user"),
43
- change_id=change_context.get("change_id"),
44
- context_detail=change_context.get("context_detail"),
45
- context=change_context.get("context"),
46
- ):
47
- for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
48
- obj._custom_field_data[field.key] = new_value
49
- obj.save()
50
- else:
51
- for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
52
- obj._custom_field_data[field.key] = new_value
53
- obj.save()
69
+ pk_list = list(queryset.values_list("pk", flat=True))
70
+ task_logger.info(
71
+ "Updating selection for custom field `%s` from `%s` to `%s` on %s records...",
72
+ field.key,
73
+ old_value,
74
+ new_value,
75
+ ct.model,
76
+ extra={"object": field},
77
+ )
78
+ count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, new_value))
79
+ task_logger.info("Updated %d records", count)
80
+ if change_context is not None:
81
+ # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
82
+ _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
54
83
 
55
84
  elif field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
56
85
  # Loop through all field content types and search for values to update
86
+ # TODO: can we implement a bulk operator for this?
57
87
  for ct in field.content_types.all():
58
88
  model = ct.model_class()
59
89
  if change_context is not None:
60
- with web_request_context(
61
- user=change_context.get("user"),
62
- change_id=change_context.get("change_id"),
63
- context_detail=change_context.get("context_detail"),
64
- context=change_context.get("context"),
65
- ):
90
+ with web_request_context(**change_context):
66
91
  for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
67
92
  old_list = obj._custom_field_data[field.key]
68
93
  new_list = [new_value if e == old_value else e for e in old_list]
@@ -76,13 +101,13 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
76
101
  obj.save()
77
102
 
78
103
  else:
79
- logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
80
- return False
104
+ task_logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
105
+ raise ValueError
81
106
 
82
107
  return True
83
108
 
84
109
 
85
- @nautobot_task
110
+ @nautobot_task(soft_time_limit=1800, time_limit=2000)
86
111
  def delete_custom_field_data(field_key, content_type_pk_set, change_context=None):
87
112
  """
88
113
  Delete the values for a custom field
@@ -90,30 +115,23 @@ def delete_custom_field_data(field_key, content_type_pk_set, change_context=None
90
115
  Args:
91
116
  field_key (str): The key of the custom field which is being deleted
92
117
  content_type_pk_set (list): List of PKs for content types to act upon
118
+ change_context (dict): Optional change context for ObjectChange creation
93
119
  """
94
- # Circular Import
95
- from nautobot.extras.context_managers import web_request_context
96
-
97
- with transaction.atomic():
98
- for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
99
- model = ct.model_class()
100
- if change_context is not None:
101
- with web_request_context(
102
- user=change_context.get("user"),
103
- change_id=change_context.get("change_id"),
104
- context_detail=change_context.get("context_detail"),
105
- context=change_context.get("context"),
106
- ):
107
- for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
108
- del obj._custom_field_data[field_key]
109
- obj.save()
110
- else:
111
- for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
112
- del obj._custom_field_data[field_key]
113
- obj.save()
114
-
115
-
116
- @nautobot_task
120
+ task_logger = getLogger("celery.task.delete_custom_field_data")
121
+ for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
122
+ model = ct.model_class()
123
+ queryset = model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False})
124
+ if change_context is not None:
125
+ pk_list = list(queryset.values_list("pk", flat=True))
126
+ task_logger.info("Deleting existing values for custom field `%s` from %s records...", field_key, ct.model)
127
+ count = queryset.update(_custom_field_data=JSONRemove("_custom_field_data", field_key))
128
+ task_logger.info("Updated %d records", count)
129
+ if change_context is not None:
130
+ # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
131
+ _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
132
+
133
+
134
+ @nautobot_task(soft_time_limit=1800, time_limit=2000)
117
135
  def provision_field(field_id, content_type_pk_set, change_context=None):
118
136
  """
119
137
  Provision a new custom field on all relevant content type object instances.
@@ -121,34 +139,35 @@ def provision_field(field_id, content_type_pk_set, change_context=None):
121
139
  Args:
122
140
  field_id (uuid4): The PK of the custom field being provisioned
123
141
  content_type_pk_set (list): List of PKs for content types to act upon
142
+ change_context (dict): Optional change context for ObjectChange creation.
124
143
  """
125
144
  # Circular Import
126
- from nautobot.extras.context_managers import web_request_context
127
145
  from nautobot.extras.models import CustomField
128
146
 
147
+ task_logger = getLogger("celery.task.provision_field")
148
+
129
149
  try:
130
150
  field = CustomField.objects.get(pk=field_id)
131
151
  except CustomField.DoesNotExist:
132
- logger.error(f"Custom field with ID {field_id} not found, failing to provision.")
133
- return False
152
+ task_logger.error(f"Custom field with ID {field_id} not found, failing to provision.")
153
+ raise
134
154
 
135
- with transaction.atomic():
136
- for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
137
- model = ct.model_class()
138
- if change_context is not None:
139
- with web_request_context(
140
- user=change_context.get("user"),
141
- change_id=change_context.get("change_id"),
142
- context_detail=change_context.get("context_detail"),
143
- context=change_context.get("context"),
144
- ):
145
- for obj in model.objects.all():
146
- obj._custom_field_data.setdefault(field.key, field.default)
147
- obj.save()
148
- else:
149
- for obj in model.objects.all():
150
- obj._custom_field_data.setdefault(field.key, field.default)
151
- obj.save()
155
+ for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
156
+ model = ct.model_class()
157
+ queryset = model.objects.filter(**{f"_custom_field_data__{field.key}__isnull": True})
158
+ if change_context is not None:
159
+ pk_list = list(queryset.values_list("pk", flat=True))
160
+ task_logger.info(
161
+ "Adding data for custom field `%s` to %s records...",
162
+ field.key,
163
+ ct.model,
164
+ extra={"object": field},
165
+ )
166
+ count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, field.default))
167
+ task_logger.info("Updated %d records.", count)
168
+ if change_context is not None:
169
+ # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
170
+ _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
152
171
 
153
172
  return True
154
173
 
@@ -277,6 +277,39 @@
277
277
  {% endif %}
278
278
  </td>
279
279
  </tr>
280
+ <tr>
281
+ <td>Table Extensions</td>
282
+ <td>
283
+ {% if features.table_extensions %}
284
+ {% if features.table_extensions.columns %}
285
+ <b>Custom Columns</b>
286
+ <ul class="list-unstyled">
287
+ {% for column in features.table_extensions.columns %}
288
+ <li><code>{{ column }}</code></li>
289
+ {% endfor %}
290
+ </ul>
291
+ {% endif %}
292
+ {% if features.table_extensions.add_to_default_columns %}
293
+ <b>Additional Default Columns</b>
294
+ <ul class="list-unstyled">
295
+ {% for column in features.table_extensions.add_to_default_columns %}
296
+ <li><code>{{ column }}</code></li>
297
+ {% endfor %}
298
+ </ul>
299
+ {% endif %}
300
+ {% if features.table_extensions.remove_from_default_columns %}
301
+ <b>Remove from Default Columns</b>
302
+ <ul class="list-unstyled">
303
+ {% for column in features.table_extensions.remove_from_default_columns %}
304
+ <li><code>{{ column }}</code></li>
305
+ {% endfor %}
306
+ </ul>
307
+ {% endif %}
308
+ {% else %}
309
+ {% include 'utilities/render_boolean.html' with value=features.table_extensions %}
310
+ {% endif %}
311
+ </td>
312
+ </tr>
280
313
  <tr>
281
314
  <td>Views/URLs</td>
282
315
  <td>
@@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
5
5
  from django.test import TestCase
6
6
 
7
7
  from nautobot.core.celery import app
8
- from nautobot.core.testing import TransactionTestCase
8
+ from nautobot.core.testing import get_job_class_and_model, TransactionTestCase
9
9
  from nautobot.core.utils.lookup import get_changes_for_model
10
10
  from nautobot.dcim.models import (
11
11
  DeviceType,
@@ -22,7 +22,7 @@ from nautobot.extras.context_managers import (
22
22
  deferred_change_logging_for_bulk_operation,
23
23
  web_request_context,
24
24
  )
25
- from nautobot.extras.models import Status, Webhook
25
+ from nautobot.extras.models import JobHook, Status, Webhook
26
26
  from nautobot.extras.utils import bulk_delete_with_bulk_change_logging
27
27
 
28
28
  # Use the proper swappable User model
@@ -74,8 +74,8 @@ class WebRequestContextTestCase(TestCase):
74
74
  self.assertEqual(oc_list[0].changed_object, location)
75
75
  self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
76
76
 
77
- @mock.patch("nautobot.extras.jobs.enqueue_job_hooks")
78
- @mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
77
+ @mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=(True, None))
78
+ @mock.patch("nautobot.extras.context_managers.enqueue_webhooks", return_value=None)
79
79
  def test_create_then_delete(self, mock_enqueue_webhooks, mock_enqueue_job_hooks):
80
80
  """Test that a create followed by a delete is logged as two changes"""
81
81
  location_type = LocationType.objects.get(name="Campus")
@@ -88,12 +88,19 @@ class WebRequestContextTestCase(TestCase):
88
88
 
89
89
  location = Location.objects.filter(pk=location_pk)
90
90
  self.assertFalse(location.exists())
91
- oc_list = get_changes_for_model(Location).filter(changed_object_id=location_pk)
91
+ oc_list = get_changes_for_model(Location).filter(changed_object_id=location_pk).order_by("time")
92
92
  self.assertEqual(len(oc_list), 2)
93
- self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_DELETE)
94
- self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_CREATE)
95
- mock_enqueue_job_hooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
96
- mock_enqueue_webhooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
93
+ self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
94
+ self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_DELETE)
95
+ mock_enqueue_job_hooks.assert_has_calls(
96
+ [
97
+ mock.call(oc_list[0], may_reload_jobs=True, jobhook_queryset=None),
98
+ mock.call(oc_list[1], may_reload_jobs=False, jobhook_queryset=None),
99
+ ],
100
+ )
101
+ mock_enqueue_webhooks.assert_has_calls(
102
+ [mock.call(oc_list[0], webhook_queryset=None), mock.call(oc_list[1], webhook_queryset=None)]
103
+ )
97
104
 
98
105
  def test_update_then_delete(self):
99
106
  """Test that an update followed by a delete is logged as a single delete"""
@@ -307,6 +314,13 @@ class BulkEditDeleteChangeLogging(TestCase):
307
314
  for i in range(1, 4)
308
315
  ]
309
316
  Location.objects.bulk_create(locations)
317
+ # Create a JobHook that applies to Locations
318
+ _, job_model = get_job_class_and_model("job_hook_receiver", "TestJobHookReceiverLog")
319
+ mock_import_jobs.assert_called_once()
320
+ mock_import_jobs.reset_mock()
321
+ job_hook = JobHook.objects.create(name="JobHookTest", type_update=True, job=job_model)
322
+ job_hook.content_types.set([ContentType.objects.get_for_model(Location)])
323
+
310
324
  pk_list = []
311
325
  with web_request_context(self.user):
312
326
  with deferred_change_logging_for_bulk_operation():
@@ -412,7 +412,7 @@ class GitTest(TransactionTestCase):
412
412
 
413
413
  def test_pull_git_repository_and_refresh_data_with_bad_data(self):
414
414
  """
415
- The test_pull_git_repository_and_refresh_data job should gracefully handle bad data in the Git repository
415
+ The test_pull_git_repository_and_refresh_data job should gracefully handle bad data in the Git repository.
416
416
  """
417
417
  with tempfile.TemporaryDirectory() as tempdir:
418
418
  with self.settings(GIT_ROOT=tempdir):
@@ -433,6 +433,18 @@ class GitTest(TransactionTestCase):
433
433
  job_result.result,
434
434
  )
435
435
 
436
+ # Due to transaction rollback on failure, the database should still/again match the pre-sync state, of
437
+ # no records owned by the repository.
438
+ self.assertFalse(ConfigContextSchema.objects.filter(owner_object_id=self.repo.id).exists())
439
+ self.assertFalse(ConfigContext.objects.filter(owner_object_id=self.repo.id).exists())
440
+ self.assertFalse(ExportTemplate.objects.filter(owner_object_id=self.repo.id).exists())
441
+ self.assertFalse(Job.objects.filter(module_name__startswith=f"{self.repo.slug}.").exists())
442
+ device = Device.objects.get(name=self.device.name)
443
+ self.assertIsNone(device.local_config_context_data)
444
+ self.assertIsNone(device.local_config_context_data_owner)
445
+ self.repo.refresh_from_db()
446
+ self.assertEqual(self.repo.current_head, "")
447
+
436
448
  # Check for specific log messages
437
449
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
438
450
  warning_logs = log_entries.filter(log_level=LogLevelChoices.LOG_WARNING)
@@ -592,6 +604,81 @@ class GitTest(TransactionTestCase):
592
604
 
593
605
  self.assert_job_exists(installed=False)
594
606
 
607
+ def test_git_repository_sync_rollback(self):
608
+ """
609
+ Once a "known-good" sync state is achieved, resync to a new "bad" head commit should fail and be rolled back.
610
+ """
611
+ with tempfile.TemporaryDirectory() as tempdir:
612
+ with self.settings(GIT_ROOT=tempdir):
613
+ # Initially have a successful sync to a good commit that provides data
614
+ self.repo.branch = "valid-files" # actually a tag
615
+ self.repo.save()
616
+ job_model = GitRepositorySync().job_model
617
+ job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
618
+ job_result.refresh_from_db()
619
+ self.assertEqual(
620
+ job_result.status,
621
+ JobResultStatusChoices.STATUS_SUCCESS,
622
+ (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
623
+ )
624
+
625
+ self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
626
+ self.assert_implicit_config_context_exists("Location context")
627
+ self.assert_config_context_schema_record_exists("Config Context Schema 1")
628
+ self.assert_device_exists(self.device.name)
629
+ self.assert_export_template_device("template.j2")
630
+ self.assert_export_template_html_exist("template2.html")
631
+ self.assert_export_template_vlan_exists("template.j2")
632
+ self.assert_job_exists(name="MyJob")
633
+ self.assert_job_exists(name="MyJobButtonReceiver")
634
+ self.assert_job_exists(name="MyJobHookReceiver")
635
+
636
+ # Create JobButton and JobHook
637
+ JobButton.objects.create(
638
+ name="MyJobButton", enabled=True, text="Click me", job=Job.objects.get(name="MyJobButtonReceiver")
639
+ )
640
+ JobHook.objects.create(name="MyJobHook", enabled=True, job=Job.objects.get(name="MyJobHookReceiver"))
641
+
642
+ self.repo.refresh_from_db()
643
+ self.assertNotEqual(self.repo.current_head, "")
644
+ good_current_head = self.repo.current_head
645
+
646
+ # Now change to the `main` branch (which includes the current commit, followed by a "bad" commit)
647
+ self.repo.branch = "main"
648
+ self.repo.save()
649
+
650
+ # Resync, attempting and failing to update to the new commit
651
+ job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
652
+ job_result.refresh_from_db()
653
+ self.assertEqual(
654
+ job_result.status,
655
+ JobResultStatusChoices.STATUS_FAILURE,
656
+ job_result.result,
657
+ )
658
+ log_entries = JobLogEntry.objects.filter(job_result=job_result)
659
+
660
+ # Assert database changes were rolled back
661
+ self.repo.refresh_from_db()
662
+ try:
663
+ self.assertEqual(self.repo.current_head, good_current_head)
664
+ self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
665
+ self.assert_implicit_config_context_exists("Location context")
666
+ self.assert_config_context_schema_record_exists("Config Context Schema 1")
667
+ self.assert_device_exists(self.device.name)
668
+ self.assert_export_template_device("template.j2")
669
+ self.assert_export_template_html_exist("template2.html")
670
+ self.assert_export_template_vlan_exists("template.j2")
671
+ self.assert_job_exists(name="MyJob")
672
+ self.assert_job_exists(name="MyJobButtonReceiver")
673
+ self.assert_job_exists(name="MyJobHookReceiver")
674
+ self.assertTrue(JobButton.objects.get(name="MyJobButton").enabled)
675
+ self.assertTrue(JobHook.objects.get(name="MyJobHook").enabled)
676
+ except Exception:
677
+ for log in log_entries:
678
+ print(log.message)
679
+ print(job_result.traceback)
680
+ raise
681
+
595
682
  def test_git_dry_run(self):
596
683
  with tempfile.TemporaryDirectory() as tempdir:
597
684
  with self.settings(GIT_ROOT=tempdir):