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
nautobot/apps/tables.py CHANGED
@@ -15,6 +15,7 @@ from nautobot.core.tables import (
15
15
  TagColumn,
16
16
  ToggleColumn,
17
17
  )
18
+ from nautobot.extras.plugins import TableExtension
18
19
  from nautobot.extras.tables import RoleTableMixin, StatusTableMixin
19
20
 
20
21
  __all__ = (
@@ -32,5 +33,6 @@ __all__ = (
32
33
  "RoleTableMixin",
33
34
  "StatusTableMixin",
34
35
  "TagColumn",
36
+ "TableExtension",
35
37
  "ToggleColumn",
36
38
  )
@@ -7,6 +7,8 @@ from nautobot.core.forms.constants import (
7
7
  NUMERIC_EXPANSION_PATTERN,
8
8
  )
9
9
  from nautobot.core.forms.fields import (
10
+ AutoPositionField,
11
+ AutoPositionPatternField,
10
12
  CommentField,
11
13
  CSVChoiceField,
12
14
  CSVContentTypeField,
@@ -80,6 +82,8 @@ __all__ = (
80
82
  "ALPHANUMERIC_EXPANSION_PATTERN",
81
83
  "APISelect",
82
84
  "APISelectMultiple",
85
+ "AutoPositionField",
86
+ "AutoPositionPatternField",
83
87
  "BOOLEAN_CHOICES",
84
88
  "BOOLEAN_WITH_BLANK_CHOICES",
85
89
  "BootstrapMixin",
@@ -444,6 +444,38 @@ class SlugField(django_forms.SlugField):
444
444
  self.widget.attrs["slug-source"] = slug_source
445
445
 
446
446
 
447
+ class AutoPositionField(django_forms.CharField):
448
+ def __init__(self, source="name", *args, **kwargs):
449
+ """
450
+ Instantiate a AutoPositionField.
451
+
452
+ Args:
453
+ source (str, tuple): Name of the field (or a list of field names) that will be used to suggest a position.
454
+ """
455
+ kwargs.setdefault("label", "Position")
456
+ kwargs.setdefault("widget", forms.SlugWidget)
457
+ super().__init__(*args, **kwargs)
458
+ if isinstance(source, (tuple, list)):
459
+ source = " ".join(source)
460
+ self.widget.attrs["source"] = source
461
+
462
+
463
+ class AutoPositionPatternField(ExpandableNameField):
464
+ def __init__(self, source="name_pattern", *args, **kwargs):
465
+ """
466
+ Instantiate a AutoPositionPatternField.
467
+
468
+ Args:
469
+ source (str, tuple): Name pattern of the field (or a list of field names) that will be used to suggest a position pattern.
470
+ """
471
+ kwargs.setdefault("label", "Position")
472
+ kwargs.setdefault("widget", forms.SlugWidget)
473
+ super().__init__(*args, **kwargs)
474
+ if isinstance(source, (tuple, list)):
475
+ source = " ".join(source)
476
+ self.widget.attrs["source"] = source
477
+
478
+
447
479
  class DynamicModelChoiceMixin:
448
480
  """
449
481
  :param display_field: The name of the attribute of an API response object to display in the selection list
@@ -19,7 +19,12 @@ from nautobot.core.jobs.cleanup import LogsCleanup
19
19
  from nautobot.core.jobs.groups import RefreshDynamicGroupCaches
20
20
  from nautobot.core.utils.lookup import get_filterset_for_model
21
21
  from nautobot.core.utils.requests import get_filterable_params_from_filter_params
22
- from nautobot.extras.datasources import ensure_git_repository, git_repository_dry_run, refresh_datasource_content
22
+ from nautobot.extras.datasources import (
23
+ ensure_git_repository,
24
+ git_repository_dry_run,
25
+ refresh_datasource_content,
26
+ refresh_job_code_from_repository,
27
+ )
23
28
  from nautobot.extras.jobs import BooleanVar, ChoiceVar, FileVar, Job, ObjectVar, RunJobTaskFailed, StringVar, TextVar
24
29
  from nautobot.extras.models import ExportTemplate, GitRepository
25
30
 
@@ -49,13 +54,24 @@ class GitRepositorySync(Job):
49
54
  self.logger.info(f'Creating/refreshing local copy of Git repository "{repository.name}"...')
50
55
 
51
56
  try:
52
- ensure_git_repository(repository, logger=self.logger)
53
- refresh_datasource_content("extras.gitrepository", repository, user, job_result, delete=False)
54
- # Given that the above succeeded, tell all workers (including ourself) to call ensure_git_repository()
55
- app.control.broadcast("refresh_git_repository", repository_pk=repository.pk, head=repository.current_head)
56
- finally:
57
- if job_result.duration:
58
- self.logger.info("Repository synchronization completed in %s", job_result.duration)
57
+ with transaction.atomic():
58
+ ensure_git_repository(repository, logger=self.logger)
59
+ refresh_datasource_content("extras.gitrepository", repository, user, job_result, delete=False)
60
+ # Given that the above succeeded, tell all workers (including ourself) to call ensure_git_repository()
61
+ app.control.broadcast(
62
+ "refresh_git_repository", repository_pk=repository.pk, head=repository.current_head
63
+ )
64
+ if job_result.duration:
65
+ self.logger.info("Repository synchronization completed in %s", job_result.duration)
66
+ except Exception:
67
+ job_result.log("Changes to database records have been reverted.")
68
+ # Re-check-out previous commit if any
69
+ repository.refresh_from_db()
70
+ if repository.current_head:
71
+ job_result.log(f"Attempting to revert local repository clone to commit {repository.current_head}")
72
+ ensure_git_repository(repository, logger=self.logger, head=repository.current_head)
73
+ refresh_job_code_from_repository(repository.slug, ignore_import_errors=True)
74
+ raise
59
75
 
60
76
 
61
77
  class GitRepositoryDryRun(Job):
@@ -1,5 +1,7 @@
1
1
  from django.db import NotSupportedError
2
- from django.db.models import Aggregate, Func, JSONField
2
+ from django.db.models import Aggregate, Func, JSONField, Value
3
+ from django.db.models.fields.json import compile_json_path
4
+ from django.db.models.functions import Cast
3
5
 
4
6
 
5
7
  class CollateAsChar(Func):
@@ -26,6 +28,150 @@ class CollateAsChar(Func):
26
28
  return super().as_sql(compiler, connection, function, template, arg_joiner, **extra_context)
27
29
 
28
30
 
31
+ class JSONSet(Func):
32
+ """
33
+ Set or create the value of a single key in a JSONField.
34
+
35
+ Example:
36
+ model.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "cf_key", "new_value"))
37
+
38
+ Limitations:
39
+ - Postgres and MySQL only.
40
+ - Does *not* support nested lookups (`key1__key2`), only a single top-level key.
41
+ - Unlike the referenced Django PR, supports only a single key/value rather than an arbitrary number of them.
42
+
43
+ References:
44
+ - https://code.djangoproject.com/ticket/32519
45
+ - https://github.com/django/django/pull/18489/files
46
+ """
47
+
48
+ function = None
49
+
50
+ def __init__(self, expression, path, value, output_field=None):
51
+ self.path = path
52
+ self.value = value
53
+ super().__init__(expression, output_field=output_field)
54
+
55
+ def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
56
+ """
57
+ Based on https://github.com/django/django/pull/18489/files.
58
+
59
+ Transforms and inserts self.path and self.value appropriately into the expression fields.
60
+ """
61
+ c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save)
62
+ # Resolve expressions in the JSON update values.
63
+ c.fields = {
64
+ self.path: (
65
+ self.value.resolve_expression(query, allow_joins, reuse, summarize, for_save)
66
+ if hasattr(self.value, "resolve_expression")
67
+ else self.value
68
+ )
69
+ }
70
+ return c
71
+
72
+ def as_sql(self, compiler, connection, function=None, **extra_context):
73
+ """
74
+ MySQL implementation based on https://github.com/django/django/pull/18489/files.
75
+
76
+ Creates a copy of this object with the appropriately transformed self.path and self.value for MySQL JSON_SET().
77
+ """
78
+ if connection.vendor != "mysql":
79
+ raise NotSupportedError(f"JSONSet is not implemented for database {connection.vendor}")
80
+
81
+ copy = self.copy()
82
+ new_source_expressions = copy.get_source_expressions()
83
+
84
+ path = compile_json_path([self.path])
85
+ value = self.value
86
+ if not hasattr(value, "resolve_expression"):
87
+ # Use Value to serialize the value to a string, then Cast to ensure it's treated as JSON.
88
+ value = Cast(Value(value, output_field=self.output_field), output_field=self.output_field)
89
+
90
+ new_source_expressions.extend((Value(path), value))
91
+ copy.set_source_expressions(new_source_expressions)
92
+ return super(JSONSet, copy).as_sql(compiler, connection, function="JSON_SET", **extra_context)
93
+
94
+ def as_postgresql(self, compiler, connection, function=None, **extra_context):
95
+ """
96
+ PostgreSQL implementation based on https://github.com/django/django/pull/18489/files.
97
+
98
+ Creates a copy of this object with appropriately transformed self.path and self.value for Postgres JSONB_SET().
99
+ """
100
+ copy = self.copy()
101
+ new_source_expressions = copy.get_source_expressions()
102
+
103
+ path = self.path
104
+ value = self.value
105
+ if not hasattr(value, "resolve_expression"):
106
+ # We don't need Cast() here because Value with a JSONFIeld is correctly handled as JSONB by Postgres
107
+ value = Value(value, output_field=self.output_field)
108
+ else:
109
+
110
+ class ToJSONB(Func):
111
+ function = "TO_JSONB"
112
+
113
+ value = ToJSONB(value, output_field=self.output_field)
114
+
115
+ new_source_expressions.extend((Value(f"{{{path}}}"), value))
116
+ copy.set_source_expressions(new_source_expressions)
117
+ return super(JSONSet, copy).as_sql(compiler, connection, function="JSONB_SET", **extra_context)
118
+
119
+
120
+ class JSONRemove(Func):
121
+ """
122
+ Unset and remove a single key in a JSONField.
123
+
124
+ Example:
125
+ model.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "cf_key"))
126
+
127
+ Limitations:
128
+ - Postgres and MySQL only.
129
+ - Does *not* support nested lookups (`key1__key2`), only a single top-level key.
130
+ - Unlike the referenced Django PR, supports only a single key, not N keys.
131
+
132
+ References:
133
+ - https://code.djangoproject.com/ticket/32519
134
+ - https://github.com/django/django/pull/18489/files
135
+ """
136
+
137
+ def __init__(self, expression, path):
138
+ self.path = path
139
+ super().__init__(expression)
140
+
141
+ def as_sql(self, compiler, connection, function=None, **extra_context):
142
+ """
143
+ MySQL implementation based on https://github.com/django/django/pull/18489/files.
144
+
145
+ Creates a copy of this object with appropriately transformed self.path for MySQL JSON_REMOVE().
146
+ """
147
+ if connection.vendor != "mysql":
148
+ raise NotSupportedError(f"JSONSet is not implemented for database {connection.vendor}")
149
+
150
+ copy = self.copy()
151
+ new_source_expressions = copy.get_source_expressions()
152
+
153
+ new_source_expressions.append(Value(compile_json_path([self.path])))
154
+
155
+ copy.set_source_expressions(new_source_expressions)
156
+ return super(JSONRemove, copy).as_sql(compiler, connection, function="JSON_REMOVE", **extra_context)
157
+
158
+ def as_postgresql(self, compiler, connection, function=None, **extra_context):
159
+ """
160
+ PostgreSQL implementation based on https://github.com/django/django/pull/18489/files.
161
+
162
+ Creates a copy of this object with appropriately transformed self.path for Postgres `#-` operator.
163
+ """
164
+ copy = self.copy()
165
+ new_source_expressions = copy.get_source_expressions()
166
+
167
+ new_source_expressions.append(Value(f"{{{self.path}}}"))
168
+
169
+ copy.set_source_expressions(new_source_expressions)
170
+ return super(JSONRemove, copy).as_sql(
171
+ compiler, connection, template="%(expressions)s", arg_joiner="#- ", **extra_context
172
+ )
173
+
174
+
29
175
  class JSONBAgg(Aggregate):
30
176
  """
31
177
  Like django.contrib.postgres.aggregates.JSONBAgg, but different.
@@ -1,9 +1,11 @@
1
1
  from django.core.cache import cache
2
2
  from django.db.models import Case, When
3
+ from django.db.models.signals import post_delete, post_save
3
4
  from tree_queries.models import TreeNode
4
5
  from tree_queries.query import TreeManager as TreeManager_, TreeQuerySet as TreeQuerySet_
5
6
 
6
7
  from nautobot.core.models import BaseManager, querysets
8
+ from nautobot.core.signals import invalidate_max_depth_cache
7
9
 
8
10
 
9
11
  class TreeQuerySet(TreeQuerySet_, querysets.RestrictedQuerySet):
@@ -117,3 +119,9 @@ class TreeModel(TreeNode):
117
119
  display_str += self.name
118
120
  cache.set(cache_key, display_str, 5)
119
121
  return display_str
122
+
123
+ @classmethod
124
+ def __init_subclass__(cls, **kwargs):
125
+ super().__init_subclass__(**kwargs)
126
+ post_save.connect(invalidate_max_depth_cache, sender=cls)
127
+ post_delete.connect(invalidate_max_depth_cache, sender=cls)
nautobot/core/settings.py CHANGED
@@ -418,7 +418,14 @@ if "NAUTOBOT_ALLOWED_HOSTS" in os.environ and os.environ["NAUTOBOT_ALLOWED_HOSTS
418
418
  ALLOWED_HOSTS = os.environ["NAUTOBOT_ALLOWED_HOSTS"].split(" ")
419
419
  else:
420
420
  ALLOWED_HOSTS = []
421
+
422
+ # Allow CSRF trusted origins to be set via an environment variable
423
+ # This allows Nautobot to be run behind a reverse proxy that terminates TLS
424
+ # and fix potential issues with CSRF validation
421
425
  CSRF_TRUSTED_ORIGINS = []
426
+ if "NAUTOBOT_CSRF_TRUSTED_ORIGINS" in os.environ and os.environ["NAUTOBOT_CSRF_TRUSTED_ORIGINS"] != "":
427
+ CSRF_TRUSTED_ORIGINS = os.getenv("NAUTOBOT_CSRF_TRUSTED_ORIGINS", "").split(_CONFIG_SETTING_SEPARATOR)
428
+
422
429
  CSRF_FAILURE_VIEW = "nautobot.core.views.csrf_failure"
423
430
  DATE_FORMAT = os.getenv("NAUTOBOT_DATE_FORMAT", "N j, Y")
424
431
  DATETIME_FORMAT = os.getenv("NAUTOBOT_DATETIME_FORMAT", "N j, Y g:i a")
@@ -587,6 +587,16 @@ properties:
587
587
  description: >-
588
588
  A list of hosts (fully-qualified domain names (FQDNs) or subdomains) that are considered trusted origins
589
589
  for cross-site secure requests such as HTTPS POST.
590
+
591
+ You might need to set this if you are using a reverse proxy or load balancer that changes the host header or if you face the error
592
+ 'Invalid Form Submission - CSRF failure has occured' and 'Origin checking failed - https://subdomain.example.com does not match any trusted origins.'
593
+
594
+ Example:
595
+
596
+ ```python
597
+ CSRF_TRUSTED_ORIGINS = ['https://subdomain.example.com', 'https://*.example.com']
598
+ ```
599
+ environment_variable: "NAUTOBOT_CSRF_TRUSTED_ORIGINS"
590
600
  items:
591
601
  type: "string"
592
602
  see_also:
nautobot/core/signals.py CHANGED
@@ -7,7 +7,6 @@ import logging
7
7
 
8
8
  from django.contrib.auth.signals import user_logged_in, user_logged_out
9
9
  from django.core.cache import cache
10
- from django.db.models.signals import post_delete, post_save
11
10
  from django.dispatch import receiver, Signal
12
11
  import redis.exceptions
13
12
 
@@ -62,10 +61,12 @@ def disable_for_loaddata(signal_handler):
62
61
  return wrapper
63
62
 
64
63
 
65
- @receiver(post_save)
66
- @receiver(post_delete)
67
64
  def invalidate_max_depth_cache(sender, **kwargs):
68
- """Clear the appropriate TreeManager.max_depth cache as the create/update/delete may have changed the tree."""
65
+ """
66
+ Clear the appropriate TreeManager.max_depth cache as the create/update/delete may have changed the tree.
67
+
68
+ Note that this signal is connected in `TreeModel.__init_subclass__()` so as to only apply to those models.
69
+ """
69
70
  from nautobot.core.models.tree_queries import TreeManager
70
71
 
71
72
  if not isinstance(sender.objects, TreeManager):
@@ -94,9 +94,13 @@ SECRET_KEY = os.getenv("NAUTOBOT_SECRET_KEY", "{{ secret_key }}")
94
94
  # FQDNs that are considered trusted origins for secure, cross-domain, requests such as HTTPS POST.
95
95
  # If running Nautobot under a single domain, you may not need to set this variable;
96
96
  # if running on multiple domains, you *may* need to set this variable to more or less the same as ALLOWED_HOSTS above.
97
+ # You also want to set this variable if you are facing CSRF validation issues such as
98
+ # 'CSRF failure has occured' or 'Origin checking failed - https://subdomain.example.com does not match any trusted origins.'
97
99
  # https://docs.djangoproject.com/en/stable/ref/settings/#csrf-trusted-origins
98
100
  #
99
101
  # CSRF_TRUSTED_ORIGINS = []
102
+ # if "NAUTOBOT_CSRF_TRUSTED_ORIGINS" in os.environ and os.environ["NAUTOBOT_CSRF_TRUSTED_ORIGINS"] != "":
103
+ # CSRF_TRUSTED_ORIGINS = os.getenv("NAUTOBOT_CSRF_TRUSTED_ORIGINS", "").split(_CONFIG_SETTING_SEPARATOR)
100
104
 
101
105
  # Date/time formatting. See the following link for supported formats:
102
106
  # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
@@ -0,0 +1,108 @@
1
+ from nautobot.core.models.query_functions import JSONRemove, JSONSet
2
+ from nautobot.core.testing import TestCase
3
+ from nautobot.dcim.models import Manufacturer
4
+
5
+
6
+ class JSONFuncTests(TestCase):
7
+ """Test JSONSet and JSONRemove functionality."""
8
+
9
+ def test_json_set(self):
10
+ # Setting a key/value should efficiently work
11
+ with self.assertNumQueries(1):
12
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", 1))
13
+ for mfr in Manufacturer.objects.all():
14
+ self.assertIn("a", mfr._custom_field_data)
15
+ self.assertEqual(1, mfr._custom_field_data["a"])
16
+
17
+ # Setting a different key/value shouldn't overwrite other keys
18
+ with self.assertNumQueries(1):
19
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "b", "text"))
20
+ for mfr in Manufacturer.objects.all():
21
+ self.assertIn("a", mfr._custom_field_data)
22
+ self.assertEqual(1, mfr._custom_field_data["a"])
23
+ self.assertIn("b", mfr._custom_field_data)
24
+ self.assertEqual("text", mfr._custom_field_data["b"])
25
+
26
+ # Setting a key/value again should overwrite that value only
27
+ with self.assertNumQueries(1):
28
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "b", "more text"))
29
+ for mfr in Manufacturer.objects.all():
30
+ self.assertIn("a", mfr._custom_field_data)
31
+ self.assertEqual(1, mfr._custom_field_data["a"])
32
+ self.assertIn("b", mfr._custom_field_data)
33
+ self.assertEqual("more text", mfr._custom_field_data["b"])
34
+
35
+ # A filtered query should be updatable
36
+ with self.assertNumQueries(1):
37
+ Manufacturer.objects.filter(name__istartswith="a").update(
38
+ _custom_field_data=JSONSet("_custom_field_data", "a", None)
39
+ )
40
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
41
+ self.assertIn("a", mfr._custom_field_data)
42
+ self.assertEqual(None, mfr._custom_field_data["a"])
43
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
44
+ self.assertIn("a", mfr._custom_field_data)
45
+ self.assertEqual(1, mfr._custom_field_data["a"])
46
+ for mfr in Manufacturer.objects.all():
47
+ self.assertIn("b", mfr._custom_field_data)
48
+ self.assertEqual("more text", mfr._custom_field_data["b"])
49
+
50
+ # Setting a value doesn't require all existing values to be homogeneous
51
+ with self.assertNumQueries(1):
52
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", "hello"))
53
+ for mfr in Manufacturer.objects.all():
54
+ self.assertIn("a", mfr._custom_field_data)
55
+ self.assertEqual("hello", mfr._custom_field_data["a"])
56
+ self.assertIn("b", mfr._custom_field_data)
57
+ self.assertEqual("more text", mfr._custom_field_data["b"])
58
+
59
+ def test_json_remove(self):
60
+ Manufacturer.objects.all().update(_custom_field_data=JSONSet("_custom_field_data", "a", 1))
61
+ Manufacturer.objects.filter(name__istartswith="a").update(
62
+ _custom_field_data=JSONSet("_custom_field_data", "b", "hello")
63
+ )
64
+ Manufacturer.objects.exclude(name__istartswith="a").update(
65
+ _custom_field_data=JSONSet("_custom_field_data", "b", "world")
66
+ )
67
+
68
+ # Should be able to clear all values for a key without impacting other keys
69
+ with self.assertNumQueries(1):
70
+ Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "a"))
71
+ for mfr in Manufacturer.objects.all():
72
+ self.assertNotIn("a", mfr._custom_field_data)
73
+ self.assertIn("b", mfr._custom_field_data)
74
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
75
+ self.assertEqual("hello", mfr._custom_field_data["b"])
76
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
77
+ self.assertEqual("world", mfr._custom_field_data["b"])
78
+
79
+ # Clearing a value that doesn't exist should be safe
80
+ with self.assertNumQueries(1):
81
+ Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "a"))
82
+ for mfr in Manufacturer.objects.all():
83
+ self.assertNotIn("a", mfr._custom_field_data)
84
+ self.assertIn("b", mfr._custom_field_data)
85
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
86
+ self.assertEqual("hello", mfr._custom_field_data["b"])
87
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
88
+ self.assertEqual("world", mfr._custom_field_data["b"])
89
+
90
+ # Subsets should be updateable
91
+ with self.assertNumQueries(1):
92
+ Manufacturer.objects.filter(name__istartswith="a").update(
93
+ _custom_field_data=JSONRemove("_custom_field_data", "b")
94
+ )
95
+ for mfr in Manufacturer.objects.all():
96
+ self.assertNotIn("a", mfr._custom_field_data)
97
+ for mfr in Manufacturer.objects.filter(name__istartswith="a"):
98
+ self.assertNotIn("b", mfr._custom_field_data)
99
+ for mfr in Manufacturer.objects.exclude(name__istartswith="a"):
100
+ self.assertIn("b", mfr._custom_field_data)
101
+ self.assertEqual("world", mfr._custom_field_data["b"])
102
+
103
+ # Non-homogeneous data should be updatable
104
+ with self.assertNumQueries(1):
105
+ Manufacturer.objects.all().update(_custom_field_data=JSONRemove("_custom_field_data", "b"))
106
+ for mfr in Manufacturer.objects.all():
107
+ self.assertNotIn("a", mfr._custom_field_data)
108
+ self.assertNotIn("b", mfr._custom_field_data)
nautobot/dcim/forms.py CHANGED
@@ -13,6 +13,8 @@ from nautobot.core.forms import (
13
13
  add_blank_choice,
14
14
  APISelect,
15
15
  APISelectMultiple,
16
+ AutoPositionField,
17
+ AutoPositionPatternField,
16
18
  BootstrapMixin,
17
19
  BulkEditNullBooleanSelect,
18
20
  ColorSelect,
@@ -269,23 +271,7 @@ class ComponentForm(BootstrapMixin, forms.Form):
269
271
 
270
272
 
271
273
  class ModularComponentForm(ComponentForm):
272
- name_pattern = ExpandableNameField(
273
- label="Name",
274
- help_text="""
275
- Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
276
- are not supported. Examples:
277
- <ul>
278
- <li><code>[ge,xe]-0/0/[0-9]</code></li>
279
- <li><code>e[0-3][a-d,f]</code></li>
280
- </ul>
281
-
282
- The variables <code>{module}</code>, <code>{module.parent}</code>, <code>{module.parent.parent}</code>, etc.
283
- may be used in the name field and will be replaced by the <code>position</code> of the module bay that the
284
- module occupies (skipping over any bays with a blank <code>position</code>). These variables can be used
285
- multiple times in the component name and there is no limit to the depth of parent levels.
286
- Any variables that cannot be replaced by a suitable position value will remain unchanged.
287
- """,
288
- )
274
+ """Base class for forms for components that can be assigned to either a device or a module."""
289
275
 
290
276
 
291
277
  #
@@ -1064,11 +1050,27 @@ class ComponentTemplateCreateForm(ComponentForm):
1064
1050
  description = forms.CharField(required=False)
1065
1051
 
1066
1052
 
1067
- class ModularComponentTemplateCreateForm(ModularComponentForm):
1053
+ class ModularComponentTemplateCreateForm(ComponentTemplateCreateForm):
1068
1054
  """
1069
1055
  Base form for the creation of modular device component templates (subclassed from ModularComponentTemplateModel).
1070
1056
  """
1071
1057
 
1058
+ name_pattern = ExpandableNameField(
1059
+ label="Name",
1060
+ help_text="""
1061
+ Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
1062
+ are not supported. Examples:
1063
+ <ul>
1064
+ <li><code>[ge,xe]-0/0/[0-9]</code></li>
1065
+ <li><code>e[0-3][a-d,f]</code></li>
1066
+ </ul>
1067
+
1068
+ The variables <code>{module}</code>, <code>{module.parent}</code>, <code>{module.parent.parent}</code>, etc.
1069
+ may be used in the name field and will be replaced by the <code>position</code> of the module bay that the
1070
+ module occupies (skipping over any bays with a blank <code>position</code>). These variables can be used
1071
+ multiple times in the component name and there is no limit to the depth of parent levels.
1072
+ Any variables that cannot be replaced by a suitable position value will remain unchanged.""",
1073
+ )
1072
1074
  device_type = DynamicModelChoiceField(
1073
1075
  queryset=DeviceType.objects.all(),
1074
1076
  required=False,
@@ -1077,7 +1079,6 @@ class ModularComponentTemplateCreateForm(ModularComponentForm):
1077
1079
  queryset=ModuleType.objects.all(),
1078
1080
  required=False,
1079
1081
  )
1080
- description = forms.CharField(required=False)
1081
1082
 
1082
1083
 
1083
1084
  class ConsolePortTemplateForm(ModularComponentTemplateForm):
@@ -1570,10 +1571,10 @@ class ModuleBayBaseCreateForm(BootstrapMixin, forms.Form):
1570
1571
  required=False,
1571
1572
  help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)",
1572
1573
  )
1573
- position_pattern = ExpandableNameField(
1574
- label="Position",
1574
+ position_pattern = AutoPositionPatternField(
1575
1575
  required=False,
1576
- help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)",
1576
+ help_text="Alphanumeric ranges are supported. (Must match the number of names being created.)"
1577
+ " Default to the names of the module bays unless manually supplied by the user.",
1577
1578
  )
1578
1579
  description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1579
1580
 
@@ -2542,12 +2543,8 @@ class DeviceBulkAddComponentForm(ComponentForm, CustomFieldModelBulkEditFormMixi
2542
2543
  nullable_fields = []
2543
2544
 
2544
2545
 
2545
- class ModuleBulkAddComponentForm(ModularComponentForm, CustomFieldModelBulkEditFormMixin):
2546
+ class ModuleBulkAddComponentForm(DeviceBulkAddComponentForm):
2546
2547
  pk = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), widget=forms.MultipleHiddenInput())
2547
- description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
2548
-
2549
- class Meta:
2550
- nullable_fields = []
2551
2548
 
2552
2549
 
2553
2550
  #
@@ -3566,6 +3563,12 @@ class ModuleBayFilterForm(NautobotFilterForm):
3566
3563
 
3567
3564
 
3568
3565
  class ModuleBayForm(NautobotModelForm):
3566
+ position = AutoPositionField(
3567
+ max_length=CHARFIELD_MAX_LENGTH,
3568
+ help_text="The position of the module bay within the parent device/module. "
3569
+ "Defaults to the name of the module bay unless overridden.",
3570
+ required=False,
3571
+ )
3569
3572
  parent_device = DynamicModelChoiceField(
3570
3573
  queryset=Device.objects.all(),
3571
3574
  required=False,
@@ -1280,3 +1280,8 @@ class ModuleBay(PrimaryModel):
1280
1280
 
1281
1281
  if not (self.parent_device or self.parent_module):
1282
1282
  raise ValidationError("Either parent_device or parent_module must be set")
1283
+
1284
+ # Populate the position field with the name of the module bay if it is not supplied by the user.
1285
+
1286
+ if not self.position:
1287
+ self.position = self.name
@@ -173,6 +173,7 @@ class DeviceTable(StatusTableMixin, RoleTableMixin, BaseTable):
173
173
  template_code="""{% if record.device_redundancy_group %}<span class="badge badge-default">{{ record.device_redundancy_group_priority|default:'None' }}</span>{% else %}—{% endif %}"""
174
174
  )
175
175
  controller_managed_device_group = tables.Column(linkify=True)
176
+ software_version = tables.Column(linkify=True, verbose_name="Software Version")
176
177
  secrets_group = tables.Column(linkify=True)
177
178
  tags = TagColumn(url_name="dcim:device_list")
178
179
 
@@ -201,6 +202,7 @@ class DeviceTable(StatusTableMixin, RoleTableMixin, BaseTable):
201
202
  "vc_priority",
202
203
  "device_redundancy_group",
203
204
  "device_redundancy_group_priority",
205
+ "software_version",
204
206
  "controller_managed_device_group",
205
207
  "secrets_group",
206
208
  "tags",
@@ -616,7 +618,7 @@ class DeviceModulePowerOutletTable(PowerOutletTable):
616
618
  row_attrs = {"class": cable_status_color_css}
617
619
 
618
620
 
619
- class BaseInterfaceTable(BaseTable):
621
+ class BaseInterfaceTable(StatusTableMixin, RoleTableMixin, BaseTable):
620
622
  enabled = BooleanColumn()
621
623
  ip_addresses = tables.TemplateColumn(
622
624
  template_code=INTERFACE_IPADDRESSES,
@@ -632,7 +634,7 @@ class BaseInterfaceTable(BaseTable):
632
634
  vrf = tables.Column(linkify=True, verbose_name="VRF")
633
635
 
634
636
 
635
- class InterfaceTable(StatusTableMixin, ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
637
+ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
636
638
  mgmt_only = BooleanColumn()
637
639
  tags = TagColumn(url_name="dcim:interface_list")
638
640