nautobot 2.3.9__py3-none-any.whl → 2.3.11__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 (335) hide show
  1. nautobot/apps/utils.py +2 -0
  2. nautobot/cloud/tables.py +1 -0
  3. nautobot/core/forms/forms.py +5 -1
  4. nautobot/core/models/query_functions.py +147 -1
  5. nautobot/core/tables.py +88 -22
  6. nautobot/core/templates/generic/object_bulk_destroy.html +12 -3
  7. nautobot/core/templates/generic/object_bulk_update.html +4 -2
  8. nautobot/core/templates/generic/object_create.html +1 -1
  9. nautobot/core/templates/rest_framework/api.html +3 -0
  10. nautobot/core/testing/api.py +3 -1
  11. nautobot/core/testing/integration.py +64 -0
  12. nautobot/core/testing/views.py +33 -27
  13. nautobot/core/tests/integration/test_app_navbar.py +3 -3
  14. nautobot/core/tests/integration/test_navbar.py +1 -1
  15. nautobot/core/tests/test_csv.py +3 -0
  16. nautobot/core/tests/test_models_query_functions.py +108 -0
  17. nautobot/core/tests/test_utils.py +25 -5
  18. nautobot/core/utils/lookup.py +35 -0
  19. nautobot/core/views/generic.py +50 -39
  20. nautobot/core/views/mixins.py +97 -43
  21. nautobot/core/views/renderers.py +8 -5
  22. nautobot/dcim/tables/devices.py +3 -0
  23. nautobot/dcim/templates/dcim/device_component_add.html +8 -8
  24. nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
  25. nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
  26. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +2 -2
  27. nautobot/dcim/templates/dcim/virtualchassis_edit.html +2 -2
  28. nautobot/dcim/tests/integration/test_create_device.py +86 -0
  29. nautobot/dcim/views.py +1 -1
  30. nautobot/extras/api/customfields.py +3 -10
  31. nautobot/extras/context_managers.py +23 -3
  32. nautobot/extras/jobs.py +20 -14
  33. nautobot/extras/models/customfields.py +12 -0
  34. nautobot/extras/signals.py +2 -0
  35. nautobot/extras/tasks.py +88 -69
  36. nautobot/extras/tests/test_context_managers.py +9 -4
  37. nautobot/extras/tests/test_relationships.py +1 -0
  38. nautobot/extras/tests/test_webhooks.py +1 -1
  39. nautobot/extras/views.py +1 -0
  40. nautobot/extras/webhooks.py +16 -7
  41. nautobot/ipam/factory.py +3 -0
  42. nautobot/ipam/filters.py +5 -0
  43. nautobot/ipam/forms.py +17 -0
  44. nautobot/ipam/models.py +2 -1
  45. nautobot/ipam/signals.py +2 -2
  46. nautobot/ipam/tables.py +3 -3
  47. nautobot/ipam/templates/ipam/ipaddress_assign.html +2 -2
  48. nautobot/ipam/tests/test_models.py +113 -1
  49. nautobot/ipam/tests/test_views.py +39 -5
  50. nautobot/project-static/docs/404.html +1 -1
  51. nautobot/project-static/docs/apps/index.html +1 -1
  52. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  53. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1 -1
  54. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1 -1
  55. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
  56. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +1 -1
  57. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1 -1
  58. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1 -1
  59. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +1 -1
  60. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1 -1
  61. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +1 -1
  62. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1 -1
  63. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1 -1
  64. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
  65. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1 -1
  66. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +62 -5
  67. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1 -1
  68. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +1 -1
  69. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1 -1
  70. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +132 -7
  71. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +176 -1
  72. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1 -1
  73. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1 -1
  74. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +95 -1
  75. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +5 -5
  76. nautobot/project-static/docs/development/apps/api/configuration-view.html +1 -1
  77. nautobot/project-static/docs/development/apps/api/database-backend-config.html +1 -1
  78. nautobot/project-static/docs/development/apps/api/models/django-admin.html +1 -1
  79. nautobot/project-static/docs/development/apps/api/models/global-search.html +1 -1
  80. nautobot/project-static/docs/development/apps/api/models/graphql.html +1 -1
  81. nautobot/project-static/docs/development/apps/api/models/index.html +1 -1
  82. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +1 -1
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +1 -1
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +1 -1
  85. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +1 -1
  86. nautobot/project-static/docs/development/apps/api/platform-features/index.html +1 -1
  87. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +1 -1
  88. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
  89. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +1 -1
  90. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +1 -1
  91. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +1 -1
  92. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +1 -1
  93. nautobot/project-static/docs/development/apps/api/prometheus.html +1 -1
  94. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  95. nautobot/project-static/docs/development/apps/api/testing.html +1 -1
  96. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +1 -1
  97. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +1 -1
  98. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +1 -1
  99. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +1 -1
  100. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +1 -1
  101. nautobot/project-static/docs/development/apps/api/views/base-template.html +1 -1
  102. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +1 -1
  103. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +1 -1
  104. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +1 -1
  105. nautobot/project-static/docs/development/apps/api/views/index.html +1 -1
  106. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -1
  107. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +1 -1
  108. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +1 -1
  109. nautobot/project-static/docs/development/apps/api/views/notes.html +1 -1
  110. nautobot/project-static/docs/development/apps/api/views/rest-api.html +1 -1
  111. nautobot/project-static/docs/development/apps/api/views/urls.html +1 -1
  112. nautobot/project-static/docs/development/apps/index.html +1 -1
  113. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  114. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
  115. nautobot/project-static/docs/development/apps/migration/from-v1.html +1 -1
  116. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +1 -1
  117. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +1 -1
  118. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +1 -1
  119. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +1 -1
  120. nautobot/project-static/docs/development/apps/porting-from-netbox.html +1 -1
  121. nautobot/project-static/docs/development/core/application-registry.html +1 -1
  122. nautobot/project-static/docs/development/core/best-practices.html +1 -1
  123. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  124. nautobot/project-static/docs/development/core/caching.html +1 -1
  125. nautobot/project-static/docs/development/core/controllers.html +1 -1
  126. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +1 -1
  127. nautobot/project-static/docs/development/core/generic-views.html +1 -1
  128. nautobot/project-static/docs/development/core/getting-started.html +1 -1
  129. nautobot/project-static/docs/development/core/homepage.html +1 -1
  130. nautobot/project-static/docs/development/core/index.html +1 -1
  131. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  132. nautobot/project-static/docs/development/core/model-features.html +1 -1
  133. nautobot/project-static/docs/development/core/natural-keys.html +1 -1
  134. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  135. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  136. nautobot/project-static/docs/development/core/role-internals.html +1 -1
  137. nautobot/project-static/docs/development/core/settings.html +1 -1
  138. nautobot/project-static/docs/development/core/style-guide.html +1 -1
  139. nautobot/project-static/docs/development/core/templates.html +1 -1
  140. nautobot/project-static/docs/development/core/testing.html +1 -1
  141. nautobot/project-static/docs/development/core/user-preferences.html +1 -1
  142. nautobot/project-static/docs/development/index.html +1 -1
  143. nautobot/project-static/docs/development/jobs/index.html +1 -1
  144. nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
  145. nautobot/project-static/docs/index.html +1 -1
  146. nautobot/project-static/docs/objects.inv +0 -0
  147. nautobot/project-static/docs/overview/application_stack.html +1 -1
  148. nautobot/project-static/docs/overview/design_philosophy.html +1 -1
  149. nautobot/project-static/docs/release-notes/index.html +1 -1
  150. nautobot/project-static/docs/release-notes/version-1.0.html +1 -1
  151. nautobot/project-static/docs/release-notes/version-1.1.html +1 -1
  152. nautobot/project-static/docs/release-notes/version-1.2.html +1 -1
  153. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  154. nautobot/project-static/docs/release-notes/version-1.4.html +1 -1
  155. nautobot/project-static/docs/release-notes/version-1.5.html +1 -1
  156. nautobot/project-static/docs/release-notes/version-1.6.html +1 -1
  157. nautobot/project-static/docs/release-notes/version-2.0.html +1 -1
  158. nautobot/project-static/docs/release-notes/version-2.1.html +1 -1
  159. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  160. nautobot/project-static/docs/release-notes/version-2.3.html +440 -140
  161. nautobot/project-static/docs/requirements.txt +1 -1
  162. nautobot/project-static/docs/search/search_index.json +1 -1
  163. nautobot/project-static/docs/sitemap.xml +270 -270
  164. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  165. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +1 -1
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +1 -1
  167. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +1 -1
  168. nautobot/project-static/docs/user-guide/administration/configuration/index.html +1 -1
  169. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  170. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +1 -1
  171. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +1 -1
  172. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +1 -1
  173. nautobot/project-static/docs/user-guide/administration/guides/docker.html +1 -1
  174. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  175. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +1 -1
  176. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +1 -1
  177. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +1 -1
  178. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +40 -1
  179. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +1 -1
  180. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +1 -1
  181. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  182. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +1 -1
  183. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +1 -1
  184. nautobot/project-static/docs/user-guide/administration/installation/index.html +1 -1
  185. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  186. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -1
  187. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
  188. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +1 -1
  189. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +1 -1
  190. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
  191. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  192. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +1 -1
  193. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +1 -1
  194. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +1 -1
  195. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +1 -1
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +1 -1
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +1 -1
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +1 -1
  199. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  200. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +1 -1
  201. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +1 -1
  202. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +1 -1
  203. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +1 -1
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +1 -1
  205. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +1 -1
  206. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +1 -1
  207. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +1 -1
  208. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +1 -1
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +1 -1
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +1 -1
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +1 -1
  212. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +1 -1
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +1 -1
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +1 -1
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +1 -1
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +1 -1
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +1 -1
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +1 -1
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +1 -1
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +1 -1
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +1 -1
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +1 -1
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +1 -1
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +1 -1
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +1 -1
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +1 -1
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +1 -1
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -1
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +1 -1
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +1 -1
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +1 -1
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +1 -1
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -1
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +1 -1
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +1 -1
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +1 -1
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +1 -1
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +1 -1
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +1 -1
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +1 -1
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +1 -1
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +1 -1
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +1 -1
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +1 -1
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +1 -1
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +1 -1
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +1 -1
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +1 -1
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +1 -1
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +1 -1
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +1 -1
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +1 -1
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +1 -1
  254. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +1 -1
  255. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +1 -1
  256. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +1 -1
  257. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +1 -1
  258. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +1 -1
  259. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +1 -1
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +1 -1
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +1 -1
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +1 -1
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +1 -1
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +1 -1
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +1 -1
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +1 -1
  267. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +1 -1
  268. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +1 -1
  269. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +1 -1
  270. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +1 -1
  271. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +1 -1
  272. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +1 -1
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +1 -1
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +1 -1
  275. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +1 -1
  276. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +1 -1
  277. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +1 -1
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -1
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +1 -1
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +1 -1
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +1 -1
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +1 -1
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +1 -1
  286. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  287. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +1 -1
  288. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +1 -1
  289. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +1 -1
  290. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +1 -1
  291. nautobot/project-static/docs/user-guide/index.html +1 -1
  292. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +1 -1
  293. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +1 -1
  294. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +1 -1
  295. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +1 -1
  296. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
  297. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +1 -1
  298. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +1 -1
  299. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +1 -1
  300. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +1 -1
  301. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +1 -1
  302. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +1 -1
  303. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +1 -1
  304. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +1 -1
  305. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +1 -1
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +1 -1
  307. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +1 -1
  308. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +1 -1
  309. nautobot/project-static/docs/user-guide/platform-functionality/note.html +1 -1
  310. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +1 -1
  311. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -1
  312. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +1 -1
  313. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +1 -1
  314. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +1 -1
  315. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +1 -1
  316. nautobot/project-static/docs/user-guide/platform-functionality/role.html +1 -1
  317. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +1 -1
  318. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +1 -1
  319. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +1 -1
  320. nautobot/project-static/docs/user-guide/platform-functionality/status.html +1 -1
  321. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +1 -1
  322. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -1
  323. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +1 -1
  324. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +1 -1
  325. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +1 -1
  326. nautobot/project-static/js/forms.js +0 -38
  327. nautobot/virtualization/forms.py +24 -0
  328. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  329. nautobot/virtualization/tests/test_views.py +7 -2
  330. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/METADATA +2 -2
  331. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/RECORD +335 -331
  332. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/LICENSE.txt +0 -0
  333. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/NOTICE +0 -0
  334. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/WHEEL +0 -0
  335. {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/entry_points.txt +0 -0
@@ -414,7 +414,7 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
414
414
  else:
415
415
  # render the form with the error message.
416
416
  data = {}
417
- if self.action in ["bulk_update", "bulk_destroy"]:
417
+ if not request.POST.get("_all") and self.action in ["bulk_update", "bulk_destroy"]:
418
418
  pk_list = self.pk_list
419
419
  table_class = self.get_table_class()
420
420
  table = table_class(queryset.filter(pk__in=pk_list), orderable=False)
@@ -578,6 +578,9 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
578
578
  if not form_class:
579
579
  if self.action == "bulk_destroy":
580
580
  queryset = self.get_queryset()
581
+ bulk_delete_all = bool(self.request.POST.get("_all"))
582
+ if bulk_delete_all:
583
+ return ConfirmationForm
581
584
 
582
585
  class BulkDestroyForm(ConfirmationForm):
583
586
  pk = ModelMultipleChoiceField(queryset=queryset, widget=MultipleHiddenInput)
@@ -909,7 +912,48 @@ class ObjectEditViewMixin(NautobotViewSetMixin, mixins.CreateModelMixin, mixins.
909
912
  return self.form_invalid(form)
910
913
 
911
914
 
912
- class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin):
915
+ class EditAndDeleteAllModelMixin:
916
+ """
917
+ UI mixin to bulk destroy all and bulk edit all model instances.
918
+ """
919
+
920
+ def _get_bulk_edit_delete_all_queryset(self, request):
921
+ """
922
+ Retrieve the queryset of model instances to be bulk-deleted or bulk-deleted, filtered based on request parameters.
923
+
924
+ This method handles the retrieval of a queryset of model instances that match the specified
925
+ filter criteria in the request parameters, allowing a bulk delete operation to be performed
926
+ on all matching instances.
927
+ """
928
+ model = self.queryset.model
929
+
930
+ # This Mixin is currently been used by both NautobotUIViewSet ObjectBulkDestroyViewMixin, ObjectBulkUpdateViewMixin
931
+ # BulkEditView, and BulkDeleteView which uses different keys for accessing filterset
932
+ filterset_class = getattr(self, "filterset", None)
933
+ if filterset_class is None:
934
+ filterset_class = getattr(self, "filterset_class", None)
935
+
936
+ if request.GET and filterset_class is not None:
937
+ queryset = filterset_class(request.GET, model.objects.all()).qs
938
+ # We take this approach because filterset.qs has already applied .distinct(),
939
+ # and performing a .delete directly on a queryset with .distinct applied is not allowed.
940
+ queryset = self.queryset.filter(pk__in=queryset)
941
+ else:
942
+ queryset = model.objects.all()
943
+ return queryset
944
+
945
+ def _bulk_delete_all_context(self, request, queryset):
946
+ model = queryset.model
947
+ return {
948
+ "obj_type_plural": model._meta.verbose_name_plural,
949
+ "return_url": self.get_return_url(request),
950
+ "total_objs_to_delete": queryset.count(),
951
+ "delete_all": True,
952
+ "table": None,
953
+ }
954
+
955
+
956
+ class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin, EditAndDeleteAllModelMixin):
913
957
  """
914
958
  UI mixin to bulk destroy model instances.
915
959
  """
@@ -923,7 +967,10 @@ class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin):
923
967
  queryset = self.get_queryset()
924
968
  model = queryset.model
925
969
  # Delete objects
926
- queryset = queryset.filter(pk__in=pk_list)
970
+ if self.request.POST.get("_all"):
971
+ queryset = self._get_bulk_edit_delete_all_queryset(self.request)
972
+ else:
973
+ queryset = queryset.filter(pk__in=pk_list)
927
974
 
928
975
  try:
929
976
  with transaction.atomic():
@@ -951,35 +998,33 @@ class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin):
951
998
  request.POST "_confirm": Function to validate the table form/BulkDestroyConfirmationForm and to perform the action of bulk destroy. Render the form with errors if exceptions are raised.
952
999
  """
953
1000
  queryset = self.get_queryset()
954
- model = queryset.model
1001
+ delete_all = bool(request.POST.get("_all"))
1002
+ data = {}
955
1003
  # Are we deleting *all* objects in the queryset or just a selected subset?
956
- if request.POST.get("_all"):
957
- if self.filterset_class is not None:
958
- self.pk_list = list(
959
- self.filterset_class(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True)
960
- )
961
- else:
962
- self.pk_list = list(model.objects.all().values_list("pk", flat=True))
1004
+ if delete_all:
1005
+ queryset = self._get_bulk_edit_delete_all_queryset(self.request)
1006
+ data = self._bulk_delete_all_context(request, queryset)
963
1007
  else:
964
1008
  self.pk_list = list(request.POST.getlist("pk"))
1009
+
965
1010
  form_class = self.get_form_class(**kwargs)
966
- data = {}
967
1011
  if "_confirm" in request.POST:
968
1012
  form = form_class(request.POST, initial=normalize_querydict(request.GET, form_class=form_class))
969
1013
  if form.is_valid():
970
1014
  return self.form_valid(form)
971
1015
  else:
972
1016
  return self.form_invalid(form)
973
- table_class = self.get_table_class()
974
- table = table_class(queryset.filter(pk__in=self.pk_list), orderable=False)
975
- if not table.rows:
976
- messages.warning(
977
- request,
978
- f"No {queryset.model._meta.verbose_name_plural} were selected for deletion.",
979
- )
980
- return redirect(self.get_return_url(request))
1017
+ if not delete_all:
1018
+ table_class = self.get_table_class()
1019
+ table = table_class(queryset.filter(pk__in=self.pk_list), orderable=False)
1020
+ if not table.rows:
1021
+ messages.warning(
1022
+ request,
1023
+ f"No {queryset.model._meta.verbose_name_plural} were selected for deletion.",
1024
+ )
1025
+ return redirect(self.get_return_url(request))
981
1026
 
982
- data.update({"table": table})
1027
+ data.update({"table": table})
983
1028
  return Response(data)
984
1029
 
985
1030
 
@@ -1037,7 +1082,7 @@ class ObjectBulkCreateViewMixin(NautobotViewSetMixin): # 3.0 TODO: remove, unus
1037
1082
  return self.form_invalid(form)
1038
1083
 
1039
1084
 
1040
- class ObjectBulkUpdateViewMixin(NautobotViewSetMixin, BulkUpdateModelMixin):
1085
+ class ObjectBulkUpdateViewMixin(NautobotViewSetMixin, BulkUpdateModelMixin, EditAndDeleteAllModelMixin):
1041
1086
  """
1042
1087
  UI mixin to bulk update model instances.
1043
1088
  """
@@ -1062,7 +1107,13 @@ class ObjectBulkUpdateViewMixin(NautobotViewSetMixin, BulkUpdateModelMixin):
1062
1107
  nullified_fields = request.POST.getlist("_nullify")
1063
1108
  with deferred_change_logging_for_bulk_operation():
1064
1109
  updated_objects = []
1065
- for obj in queryset.filter(pk__in=form.cleaned_data["pk"]):
1110
+ edit_all = self.request.POST.get("_all")
1111
+
1112
+ if edit_all:
1113
+ queryset = self._get_bulk_edit_delete_all_queryset(self.request)
1114
+ else:
1115
+ queryset = queryset.filter(pk__in=form.cleaned_data["pk"])
1116
+ for obj in queryset:
1066
1117
  self.obj = obj
1067
1118
  # Update standard fields. If a field is listed in _nullify, delete its value.
1068
1119
  for name in standard_fields:
@@ -1133,38 +1184,41 @@ class ObjectBulkUpdateViewMixin(NautobotViewSetMixin, BulkUpdateModelMixin):
1133
1184
  request.POST "_edit": Function to render the user selection of objects in a table form/BulkUpdateForm via Response that is passed to NautobotHTMLRenderer.
1134
1185
  request.POST "_apply": Function to validate the table form/BulkUpdateForm and to perform the action of bulk update. Render the form with errors if exceptions are raised.
1135
1186
  """
1136
- queryset = self.get_queryset()
1137
- model = queryset.model
1187
+ edit_all = request.POST.get("_all")
1138
1188
 
1139
- # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
1140
- if request.POST.get("_all"):
1141
- if self.filterset_class is not None:
1142
- self.pk_list = list(
1143
- self.filterset_class(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True)
1144
- )
1145
- else:
1146
- self.pk_list = list(model.objects.all().values_list("pk", flat=True))
1189
+ if edit_all:
1190
+ self.pk_list = None
1191
+ queryset = self._get_bulk_edit_delete_all_queryset(request)
1147
1192
  else:
1148
1193
  self.pk_list = list(request.POST.getlist("pk"))
1194
+ queryset = self.get_queryset().filter(pk__in=self.pk_list)
1195
+
1149
1196
  data = {}
1150
1197
  form_class = self.get_form_class()
1151
1198
  if "_apply" in request.POST:
1152
1199
  self.kwargs = kwargs
1153
- form = form_class(queryset.model, request.POST)
1200
+ form = form_class(queryset.model, request.POST, edit_all=edit_all)
1154
1201
  restrict_form_fields(form, request.user)
1155
1202
  if form.is_valid():
1156
1203
  return self.form_valid(form)
1157
1204
  else:
1158
1205
  return self.form_invalid(form)
1159
- table_class = self.get_table_class()
1160
- table = table_class(queryset.filter(pk__in=self.pk_list), orderable=False)
1161
- if not table.rows:
1162
- messages.warning(
1163
- request,
1164
- f"No {queryset.model._meta.verbose_name_plural} were selected to update.",
1165
- )
1166
- return redirect(self.get_return_url(request))
1167
- data.update({"table": table})
1206
+ table = None
1207
+ if not edit_all:
1208
+ table_class = self.get_table_class()
1209
+ table = table_class(queryset, orderable=False)
1210
+ if not table.rows:
1211
+ messages.warning(
1212
+ request,
1213
+ f"No {queryset.model._meta.verbose_name_plural} were selected to update.",
1214
+ )
1215
+ return redirect(self.get_return_url(request))
1216
+ data.update(
1217
+ {
1218
+ "table": table,
1219
+ "objs_count": queryset.count(),
1220
+ }
1221
+ )
1168
1222
  return Response(data)
1169
1223
 
1170
1224
 
@@ -249,19 +249,22 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
249
249
  "return_url": return_url,
250
250
  }
251
251
  form = form_class(initial=initial)
252
- table = self.construct_table(view, pk_list=pk_list)
252
+ delete_all = request.POST.get("_all")
253
+ if not delete_all:
254
+ table = self.construct_table(view, pk_list=pk_list)
253
255
  elif view.action == "bulk_create": # 3.0 TODO: remove, replaced by ImportObjects system Job
254
256
  form = view.get_form()
255
257
  if request.data:
256
258
  table = data.get("table")
257
259
  elif view.action == "bulk_update":
260
+ edit_all = request.POST.get("_all")
258
261
  pk_list = getattr(view, "pk_list", [])
259
- if pk_list:
262
+ if pk_list or edit_all:
260
263
  initial_data = {"pk": pk_list}
261
- form = form_class(model, initial=initial_data)
262
-
264
+ form = form_class(model, initial=initial_data, edit_all=edit_all)
263
265
  restrict_form_fields(form, request.user)
264
- table = self.construct_table(view, pk_list=pk_list)
266
+ if not edit_all:
267
+ table = self.construct_table(view, pk_list=pk_list)
265
268
  elif view.action == "notes":
266
269
  initial_data = {
267
270
  "assigned_object_type": content_type,
@@ -1277,6 +1277,9 @@ class SoftwareImageFileTable(StatusTableMixin, BaseTable):
1277
1277
  class SoftwareVersionTable(StatusTableMixin, BaseTable):
1278
1278
  pk = ToggleColumn()
1279
1279
  version = tables.Column(linkify=True)
1280
+ platform = tables.Column(linkify=True)
1281
+ release_date = tables.DateColumn()
1282
+ end_of_support_date = tables.DateColumn()
1280
1283
  software_image_file_count = LinkedCountColumn(
1281
1284
  viewname="dcim:softwareimagefile_list",
1282
1285
  url_params={"software_version": "pk"},
@@ -7,7 +7,7 @@
7
7
  <form action="" method="post" class="form form-horizontal">
8
8
  {% csrf_token %}
9
9
  <div class="row">
10
- <div class="col-md-6 col-md-offset-3">
10
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
11
11
  {% if form.non_field_errors %}
12
12
  <div class="panel panel-danger">
13
13
  <div class="panel-heading"><strong>Errors</strong></div>
@@ -25,13 +25,13 @@
25
25
  </div>
26
26
  </div>
27
27
  {% include 'inc/extras_features_edit_form_fields.html' with form=model_form %}
28
- <div class="form-group">
29
- <div class="col-md-9 col-md-offset-3 text-right">
30
- <button type="submit" name="_create" class="btn btn-primary">Create</button>
31
- <button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
32
- <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
33
- </div>
34
- </div>
28
+ </div>
29
+ </div>
30
+ <div class="row">
31
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-right">
32
+ <button type="submit" name="_create" class="btn btn-primary">Create</button>
33
+ <button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
34
+ <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
35
35
  </div>
36
36
  </div>
37
37
  </form>
@@ -0,0 +1,39 @@
1
+ {% extends 'dcim/device_component_add.html' %}
2
+
3
+ {% block javascript %}
4
+ {{ block.super }}
5
+ <script>
6
+ document.addEventListener('DOMContentLoaded', function() {
7
+ var position_field = document.getElementById('id_position_pattern');
8
+ var source_arr = position_field.getAttribute('source').split(" ");
9
+ var length = position_field.getAttribute('maxlength');
10
+ position_field.setAttribute('_changed', Boolean(position_field.value))
11
+ position_field.addEventListener('change', function() {
12
+ position_field.setAttribute('_changed', Boolean(position_field.value))
13
+ });
14
+ function repopulate() {
15
+ let str = "";
16
+ for (source_str of source_arr) {
17
+ if (str != "") {
18
+ str += " ";
19
+ }
20
+ let source_id = 'id_' + source_str;
21
+ let source = document.getElementById(source_id)
22
+ str += source.value;
23
+ }
24
+ position_field.value = str.slice(0, length ? length : 255);
25
+ };
26
+ for (source_str of source_arr) {
27
+ let source_id = 'id_' + source_str;
28
+ let source = document.getElementById(source_id);
29
+ source.addEventListener('keyup', function() {
30
+ if (position_field && position_field.getAttribute('_changed')=="false") {
31
+ repopulate();
32
+ }
33
+ });
34
+ }
35
+ document.getElementsByClassName('reslugify')[0].addEventListener('click', repopulate);
36
+ document.getElementsByClassName('reslugify')[0].setAttribute('data-original-title', "Regenerate position");
37
+ });
38
+ </script>
39
+ {% endblock javascript %}
@@ -0,0 +1,39 @@
1
+ {% extends 'generic/object_create.html' %}
2
+
3
+ {% block javascript %}
4
+ {{ block.super }}
5
+ <script>
6
+ document.addEventListener('DOMContentLoaded', function() {
7
+ var position_field = document.getElementById('id_position');
8
+ var source_arr = position_field.getAttribute('source').split(" ");
9
+ var length = position_field.getAttribute('maxlength');
10
+ position_field.setAttribute('_changed', Boolean(position_field.value))
11
+ position_field.addEventListener('change', function() {
12
+ position_field.setAttribute('_changed', Boolean(position_field.value))
13
+ });
14
+ function repopulate() {
15
+ let str = "";
16
+ for (source_str of source_arr) {
17
+ if (str != "") {
18
+ str += " ";
19
+ }
20
+ let source_id = 'id_' + source_str;
21
+ let source = document.getElementById(source_id)
22
+ str += source.value;
23
+ }
24
+ position_field.value = str.slice(0, length ? length : 255);
25
+ };
26
+ for (source_str of source_arr) {
27
+ let source_id = 'id_' + source_str;
28
+ let source = document.getElementById(source_id);
29
+ source.addEventListener('keyup', function() {
30
+ if (position_field && position_field.getAttribute('_changed')=="false") {
31
+ repopulate();
32
+ }
33
+ });
34
+ }
35
+ document.getElementsByClassName('reslugify')[0].addEventListener('click', repopulate);
36
+ document.getElementsByClassName('reslugify')[0].setAttribute('data-original-title', "Regenerate position");
37
+ });
38
+ </script>
39
+ {% endblock javascript %}
@@ -5,7 +5,7 @@
5
5
  <form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
6
6
  {% csrf_token %}
7
7
  <div class="row">
8
- <div class="col-md-6 col-md-offset-3">
8
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
9
9
  <h3>{% block title %}Add New Member to Virtual Chassis {{ virtual_chassis }}{% endblock %}</h3>
10
10
  {% if membership_form.non_field_errors %}
11
11
  <div class="panel panel-danger">
@@ -25,7 +25,7 @@
25
25
  </div>
26
26
  </div>
27
27
  <div class="row">
28
- <div class="col-md-6 col-md-offset-3 text-right">
28
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-right">
29
29
  <button type="submit" name="_save" class="btn btn-primary">Save</button>
30
30
  <button type="submit" name="_addanother" class="btn btn-primary">Add Another</button>
31
31
  <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
@@ -8,7 +8,7 @@
8
8
  {{ pk_form.pk }}
9
9
  {{ formset.management_form }}
10
10
  <div class="row">
11
- <div class="col-md-8 col-md-offset-2">
11
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
12
12
  <h3>{% block title %}{% if vc_form.instance %}Editing {{ vc_form.instance }}{% else %}New Virtual Chassis{% endif %}{% endblock %}</h3>
13
13
  {% if vc_form.non_field_errors %}
14
14
  <div class="panel panel-danger">
@@ -84,7 +84,7 @@
84
84
  </div>
85
85
  </div>
86
86
  <div class="row">
87
- <div class="col-md-8 col-md-offset-2 text-right">
87
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-right">
88
88
  <button type="submit" name="_update" class="btn btn-primary">Update</button>
89
89
  <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
90
90
  </div>
@@ -0,0 +1,86 @@
1
+ from django.urls import reverse
2
+
3
+ from nautobot.core.testing.integration import SeleniumTestCase
4
+
5
+
6
+ class CreateDeviceTestCase(SeleniumTestCase):
7
+ """
8
+ Create a device and all pre-requisite objects through the UI.
9
+ """
10
+
11
+ def test_create_device(self):
12
+ """
13
+ This test goes through the process of creating a device in the UI. All pre-requisite objects are created:
14
+ - Manufacturer
15
+ - Device Type
16
+ - LocationType
17
+ - Location
18
+ - Role
19
+ - Device
20
+
21
+ """
22
+ self.user.is_superuser = True
23
+ self.user.save()
24
+ self.login(self.user.username, self.password)
25
+
26
+ # Manufacturer
27
+ self.click_navbar_entry("Devices", "Manufacturers")
28
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:manufacturer_list"))
29
+ self.click_list_view_add_button()
30
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:manufacturer_add"))
31
+ self.browser.fill("name", "Test Manufacturer 1")
32
+ self.click_edit_form_create_button()
33
+
34
+ # Device Type
35
+ self.click_navbar_entry("Devices", "Device Types")
36
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:devicetype_list"))
37
+ self.click_list_view_add_button()
38
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:devicetype_add"))
39
+ self.fill_select2_field("manufacturer", "Test Manufacturer 1")
40
+ self.browser.fill("model", "Test Device Type 1")
41
+ self.click_edit_form_create_button()
42
+
43
+ # LocationType
44
+ self.click_navbar_entry("Organization", "Location Types")
45
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:locationtype_list"))
46
+ self.click_list_view_add_button()
47
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:locationtype_add"))
48
+ self.fill_select2_multiselect_field("content_types", "dcim | device")
49
+ self.browser.fill("name", "Test Location Type 1")
50
+ self.click_edit_form_create_button()
51
+
52
+ # Location
53
+ self.click_navbar_entry("Organization", "Locations")
54
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_list"))
55
+ self.click_list_view_add_button()
56
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:location_add"))
57
+ self.fill_select2_field("location_type", "Test Location Type 1")
58
+ self.fill_select2_field("status", "") # pick first status
59
+ self.browser.fill("name", "Test Location 1")
60
+ self.click_edit_form_create_button()
61
+
62
+ # Role
63
+ self.click_navbar_entry("Organization", "Roles")
64
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("extras:role_list"))
65
+ self.click_list_view_add_button()
66
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("extras:role_add"))
67
+ self.browser.fill("name", "Test Role 1")
68
+ self.fill_select2_multiselect_field("content_types", "dcim | device")
69
+ self.click_edit_form_create_button()
70
+
71
+ # Device
72
+ self.click_navbar_entry("Devices", "Devices")
73
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_list"))
74
+ self.click_list_view_add_button()
75
+ self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:device_add"))
76
+ self.browser.fill("name", "Test Device Integration Test 1")
77
+ self.fill_select2_field("role", "Test Role 1")
78
+ self.fill_select2_field("device_type", "Test Device Type 1")
79
+ self.fill_select2_field("location", "Test Location 1")
80
+ self.fill_select2_field("status", "") # pick first status
81
+ self.click_edit_form_create_button()
82
+
83
+ # Assert that the device was created
84
+ self.assertTrue(self.browser.is_text_present("Created device Test Device Integration Test 1", wait_time=5))
85
+ self.assertTrue(self.browser.is_text_present("Test Location 1", wait_time=5))
86
+ self.assertTrue(self.browser.is_text_present("Test Device Type 1", wait_time=5))
nautobot/dcim/views.py CHANGED
@@ -3241,7 +3241,7 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3241
3241
  model_form_class = forms.ModuleBayForm
3242
3242
  serializer_class = serializers.ModuleBaySerializer
3243
3243
  table_class = tables.ModuleBayTable
3244
- create_template_name = "dcim/device_component_add.html"
3244
+ create_template_name = "dcim/modulebay_create.html"
3245
3245
 
3246
3246
  def get_extra_context(self, request, instance):
3247
3247
  if instance:
@@ -40,15 +40,7 @@ class CustomFieldDefaultValues:
40
40
  class CustomFieldsDataField(Field):
41
41
  @property
42
42
  def custom_field_keys(self):
43
- """
44
- Cache CustomField keys assigned to this model to avoid redundant database queries
45
- """
46
- if not hasattr(self, "_custom_field_keys"):
47
- content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
48
- self._custom_field_keys = CustomField.objects.filter(content_types=content_type).values_list(
49
- "key", flat=True
50
- )
51
- return self._custom_field_keys
43
+ return CustomField.objects.keys_for_model(self.parent.Meta.model)
52
44
 
53
45
  def to_representation(self, obj):
54
46
  return {key: obj.get(key) for key in self.custom_field_keys}
@@ -58,7 +50,8 @@ class CustomFieldsDataField(Field):
58
50
 
59
51
  # Discard any entries in data that do not align with actual CustomFields - this matches the REST API behavior
60
52
  # for top-level serializer fields that do not exist or are not writable
61
- data = {key: value for key, value in data.items() if key in self.custom_field_keys}
53
+ custom_field_keys = self.custom_field_keys
54
+ data = {key: value for key, value in data.items() if key in custom_field_keys}
62
55
 
63
56
  # If updating an existing instance, start with existing _custom_field_data
64
57
  if self.parent.instance:
@@ -204,14 +204,34 @@ def web_request_context(
204
204
  yield request
205
205
  finally:
206
206
  jobs_reloaded = False
207
+ # In bulk operations, we are performing the same action (create/update/delete) on the same content-type.
208
+ # Save some repeated database queries by reusing the same evaluated querysets where applicable:
209
+ jobhook_queryset = None
210
+ webhook_queryset = None
211
+ last_action = None
212
+ last_content_type = None
207
213
  # enqueue jobhooks and webhooks, use change_context.change_id in case change_id was not supplied
208
214
  for object_change in (
209
- ObjectChange.objects.filter(request_id=change_context.change_id).order_by("time").iterator()
215
+ ObjectChange.objects.select_related("changed_object_type", "user")
216
+ .filter(request_id=change_context.change_id)
217
+ .order_by("time") # default ordering is -time but we want oldest first not newest first
218
+ .iterator()
210
219
  ):
220
+ if object_change.action != last_action or object_change.changed_object_type != last_content_type:
221
+ jobhook_queryset = None
222
+ webhook_queryset = None
223
+
211
224
  if context != ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
212
225
  # Make sure JobHooks are up to date (only once) before calling them
213
- jobs_reloaded |= enqueue_job_hooks(object_change, may_reload_jobs=(not jobs_reloaded))
214
- enqueue_webhooks(object_change)
226
+ did_reload_jobs, jobhook_queryset = enqueue_job_hooks(
227
+ object_change, may_reload_jobs=(not jobs_reloaded), jobhook_queryset=jobhook_queryset
228
+ )
229
+ if did_reload_jobs:
230
+ jobs_reloaded = True
231
+
232
+ webhook_queryset = enqueue_webhooks(object_change, webhook_queryset=webhook_queryset)
233
+ last_action = object_change.action
234
+ last_content_type = object_change.changed_object_type
215
235
 
216
236
 
217
237
  @contextmanager
nautobot/extras/jobs.py CHANGED
@@ -1144,41 +1144,47 @@ def run_job(self, job_class_path, *args, **kwargs):
1144
1144
  raise
1145
1145
 
1146
1146
 
1147
- def enqueue_job_hooks(object_change, may_reload_jobs=True):
1147
+ def enqueue_job_hooks(object_change, may_reload_jobs=True, jobhook_queryset=None):
1148
1148
  """
1149
1149
  Find job hook(s) assigned to this changed object type + action and enqueue them to be processed.
1150
1150
 
1151
+ Args:
1152
+ object_change (ObjectChange): The change that may trigger JobHooks to execute.
1153
+ may_reload_jobs (bool): Whether to reload JobHook source code from disk to guarantee up-to-date code.
1154
+ jobhook_queryset (QuerySet): Previously retrieved set of JobHooks to potentially enqueue
1155
+
1151
1156
  Returns:
1152
- jobs_reloaded (bool): whether Jobs were reloaded to make this happen
1157
+ result (tuple[bool, QuerySet]): whether Jobs were reloaded here, and the jobhooks that were considered
1153
1158
  """
1154
1159
  jobs_reloaded = False
1155
1160
 
1156
1161
  # Job hooks cannot trigger other job hooks
1157
1162
  if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
1158
- return jobs_reloaded
1163
+ return jobs_reloaded, jobhook_queryset
1159
1164
 
1160
1165
  # Determine whether this type of object supports job hooks
1161
1166
  content_type = object_change.changed_object_type
1162
1167
  if content_type not in change_logged_models_queryset():
1163
- return jobs_reloaded
1168
+ return jobs_reloaded, jobhook_queryset
1164
1169
 
1165
1170
  # Retrieve any applicable job hooks
1166
- action_flag = {
1167
- ObjectChangeActionChoices.ACTION_CREATE: "type_create",
1168
- ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
1169
- ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
1170
- }[object_change.action]
1171
- job_hooks = JobHook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
1171
+ if jobhook_queryset is None:
1172
+ action_flag = {
1173
+ ObjectChangeActionChoices.ACTION_CREATE: "type_create",
1174
+ ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
1175
+ ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
1176
+ }[object_change.action]
1177
+ jobhook_queryset = JobHook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
1172
1178
 
1173
- if not job_hooks.exists():
1174
- return jobs_reloaded
1179
+ if not jobhook_queryset: # not .exists() as we *want* to populate the queryset cache
1180
+ return jobs_reloaded, jobhook_queryset
1175
1181
 
1176
1182
  # Enqueue the jobs related to the job_hooks
1177
1183
  if may_reload_jobs:
1178
1184
  get_jobs(reload=True)
1179
1185
  jobs_reloaded = True
1180
1186
 
1181
- for job_hook in job_hooks:
1187
+ for job_hook in jobhook_queryset:
1182
1188
  job_model = job_hook.job
1183
1189
  if not job_model.installed or not job_model.enabled:
1184
1190
  logger.warning(
@@ -1189,4 +1195,4 @@ def enqueue_job_hooks(object_change, may_reload_jobs=True):
1189
1195
  else:
1190
1196
  JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)
1191
1197
 
1192
- return jobs_reloaded
1198
+ return jobs_reloaded, jobhook_queryset