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
nautobot/apps/utils.py CHANGED
@@ -30,6 +30,7 @@ from nautobot.core.utils.lookup import (
30
30
  get_form_for_model,
31
31
  get_model_from_name,
32
32
  get_related_class_for_model,
33
+ get_related_field_for_models,
33
34
  get_route_for_model,
34
35
  get_table_for_model,
35
36
  get_url_for_url_pattern,
@@ -111,6 +112,7 @@ __all__ = (
111
112
  "get_only_new_ui_ready_routes",
112
113
  "get_permission_for_model",
113
114
  "get_related_class_for_model",
115
+ "get_related_field_for_models",
114
116
  "get_route_for_model",
115
117
  "get_settings_or_config",
116
118
  "get_table_for_model",
nautobot/cloud/tables.py CHANGED
@@ -55,6 +55,7 @@ class CloudNetworkTable(BaseTable):
55
55
  circuit_count = LinkedCountColumn(
56
56
  viewname="circuits:circuit_list",
57
57
  url_params={"cloud_network": "name"},
58
+ # lookup="circuit_terminations__circuit", # TODO: not currently supported
58
59
  verbose_name="Circuits",
59
60
  reverse_lookup="circuit_terminations__cloud_network",
60
61
  )
@@ -113,7 +113,7 @@ class BulkEditForm(forms.Form):
113
113
  a more powerful subclass and should be used instead of directly inheriting from this class.
114
114
  """
115
115
 
116
- def __init__(self, model, *args, **kwargs):
116
+ def __init__(self, model, *args, edit_all=False, **kwargs):
117
117
  super().__init__(*args, **kwargs)
118
118
  self.model = model
119
119
  self.nullable_fields = []
@@ -122,6 +122,10 @@ class BulkEditForm(forms.Form):
122
122
  if hasattr(self.Meta, "nullable_fields"):
123
123
  self.nullable_fields = self.Meta.nullable_fields
124
124
 
125
+ if edit_all:
126
+ self.fields["pk"].required = False
127
+ self.fields["_all"] = forms.BooleanField(widget=forms.HiddenInput(), required=True, initial=True)
128
+
125
129
 
126
130
  class BulkRenameForm(forms.Form):
127
131
  """
@@ -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.
nautobot/core/tables.py CHANGED
@@ -5,10 +5,12 @@ from django.contrib.auth.models import AnonymousUser
5
5
  from django.contrib.contenttypes.fields import GenericForeignKey
6
6
  from django.core.exceptions import FieldDoesNotExist, FieldError
7
7
  from django.db import NotSupportedError
8
+ from django.db.models import Prefetch
8
9
  from django.db.models.fields.related import ForeignKey, RelatedField
9
10
  from django.db.models.fields.reverse_related import ManyToOneRel
10
11
  from django.urls import reverse
11
12
  from django.utils.html import escape, format_html, format_html_join
13
+ from django.utils.http import urlencode
12
14
  from django.utils.safestring import mark_safe
13
15
  from django.utils.text import Truncator
14
16
  import django_tables2
@@ -18,7 +20,7 @@ from tree_queries.models import TreeNode
18
20
 
19
21
  from nautobot.core.models.querysets import count_related
20
22
  from nautobot.core.templatetags import helpers
21
- from nautobot.core.utils import lookup
23
+ from nautobot.core.utils.lookup import get_model_for_view_name, get_related_field_for_models, get_route_for_model
22
24
  from nautobot.extras import choices, models
23
25
 
24
26
  logger = logging.getLogger(__name__)
@@ -154,12 +156,25 @@ class BaseTable(django_tables2.Table):
154
156
  if not column.visible:
155
157
  continue
156
158
  if isinstance(column.column, LinkedCountColumn):
157
- column_model = lookup.get_model_for_view_name(column.column.viewname)
159
+ column_model = get_model_for_view_name(column.column.viewname)
158
160
  if column_model is None:
159
161
  logger.error("Couldn't find model for %s", column.column.viewname)
160
162
  continue
161
163
  reverse_lookup = column.column.reverse_lookup or next(iter(column.column.url_params.keys()))
162
164
  count_fields.append((column.name, column_model, reverse_lookup))
165
+ try:
166
+ lookup = column.column.lookup or get_related_field_for_models(model, column_model).name
167
+ # For some reason get_related_field_for_models(Tag, DynamicGroup) gives a M2M with the name
168
+ # `dynamicgroup`, which isn't actually a field on Tag. May be a django-taggit issue?
169
+ # Workaround for now: make sure the field actually exists on the model under this name:
170
+ getattr(model, lookup)
171
+ except AttributeError:
172
+ lookup = None
173
+ if lookup is not None:
174
+ # Also attempt to prefetch the first matching record for display - see LinkedCountColumn
175
+ prefetch_fields.append(
176
+ Prefetch(lookup, column_model.objects.all()[:1], to_attr=f"{lookup}_list")
177
+ )
163
178
  continue
164
179
 
165
180
  column_model = model
@@ -233,7 +248,7 @@ class BaseTable(django_tables2.Table):
233
248
  # Belt and suspenders - we should have avoided any error cases above, but be safe anyway:
234
249
  try:
235
250
  queryset = queryset.prefetch_related(*prefetch_fields)
236
- except (TypeError, ValueError, NotSupportedError) as exc:
251
+ except (AttributeError, TypeError, ValueError, NotSupportedError) as exc:
237
252
  logger.warning(
238
253
  "Unexpected error when trying to .prefetch_related() on %s QuerySet: %s",
239
254
  model.__name__,
@@ -395,9 +410,9 @@ class ButtonsColumn(django_tables2.TemplateColumn):
395
410
  self.template_code = prepend_template + self.template_code
396
411
 
397
412
  app_label = model._meta.app_label
398
- changelog_route = lookup.get_route_for_model(model, "changelog")
399
- edit_route = lookup.get_route_for_model(model, "edit")
400
- delete_route = lookup.get_route_for_model(model, "delete")
413
+ changelog_route = get_route_for_model(model, "changelog")
414
+ edit_route = get_route_for_model(model, "edit")
415
+ delete_route = get_route_for_model(model, "delete")
401
416
 
402
417
  template_code = self.template_code.format(
403
418
  app_label=app_label,
@@ -462,29 +477,80 @@ class ColoredLabelColumn(django_tables2.TemplateColumn):
462
477
 
463
478
  class LinkedCountColumn(django_tables2.Column):
464
479
  """
465
- Render a count of related objects linked to a filtered URL.
466
-
467
- :param viewname: The view name to use for URL resolution
468
- :param view_kwargs: Additional kwargs to pass for URL resolution (optional)
469
- :param url_params: A dict of query parameters to append to the URL (e.g. ?foo=bar) (optional)
470
- :param reverse_lookup: The reverse lookup parameter to use to derive the count. If not specified, the first key
471
- in `url_params` will be implicitly used as the `reverse_lookup` value.
480
+ Render a count of related objects linked to a filtered URL, or if a single related object is present, the object.
481
+
482
+ Args:
483
+ viewname (str): The list view name to use for URL resolution, for example `"dcim:location_list"`
484
+ url_params (dict, optional): Query parameters to apply to filter the list URL (e.g. `{"vlans": "pk"}` will add
485
+ `?vlans=<record.pk>` to the linked list URL)
486
+ view_kwargs (dict, optional): Additional kwargs to pass to `reverse()` for list URL resolution. Rarely used.
487
+ lookup (str, optional): The field name on the base record that can be used to query the related objects.
488
+ If not specified, `nautobot.core.utils.lookup.get_related_field_for_models()` will be called at render time
489
+ to attempt to intelligently find the appropriate field.
490
+ TODO: this currently does *not* support nested lookups via `__`. That may be solvable in the future.
491
+ reverse_lookup (str, optional): The reverse lookup parameter to use to derive the count.
492
+ If not specified, the first key in `url_params` will be implicitly used as the `reverse_lookup` value.
493
+ **kwargs (dict, optional): As the parent Column class.
494
+
495
+ Examples:
496
+ ```py
497
+ class VLANTable(..., BaseTable):
498
+ ...
499
+ location_count = LinkedCountColumn(
500
+ # Link for N related locations will be reverse("dcim:location_list") + "?vlans=<record.pk>"
501
+ viewname="dcim:location_list",
502
+ url_params={"vlans": "pk"},
503
+ verbose_name="Locations",
504
+ )
505
+ ```
506
+
507
+ ```py
508
+ class CloudNetworkTable(BaseTable):
509
+ ...
510
+ circuit_count = LinkedCountColumn(
511
+ # Link for N related circuits will be reverse("circuits:circuit_list") + "?cloud_network=<record.name>"
512
+ viewname="circuits:circuit_list",
513
+ url_params={"cloud_network": "name"},
514
+ # We'd like to do the below but this module isn't currently smart enough to build the right Prefetch()
515
+ # for a nested lookup:
516
+ # lookup="circuit_terminations__circuit",
517
+ # For the count, .annotate(circuit_count=count_related(Circuit, "circuit_terminations__cloud_network"))
518
+ reverse_lookup="circuit_terminations__cloud_network",
519
+ verbose_name="Circuits",
520
+ )
521
+ ```
472
522
  """
473
523
 
474
- def __init__(self, viewname, *args, view_kwargs=None, url_params=None, reverse_lookup=None, default=0, **kwargs):
524
+ def __init__(
525
+ self, viewname, *args, view_kwargs=None, url_params=None, lookup=None, reverse_lookup=None, default=0, **kwargs
526
+ ):
475
527
  self.viewname = viewname
528
+ self.lookup = lookup
476
529
  self.view_kwargs = view_kwargs or {}
477
530
  self.url_params = url_params
478
- self.reverse_lookup = reverse_lookup
531
+ self.reverse_lookup = reverse_lookup or next(iter(url_params.keys()))
532
+ self.model = get_model_for_view_name(self.viewname)
479
533
  super().__init__(*args, default=default, **kwargs)
480
534
 
481
- def render(self, record, value): # pylint: disable=arguments-differ
482
- if value:
483
- url = reverse(self.viewname, kwargs=self.view_kwargs)
484
- if self.url_params:
485
- url += "?" + "&".join([f"{k}={getattr(record, v)}" for k, v in self.url_params.items()])
486
- return format_html('<a href="{}">{}</a>', url, value)
487
- return value
535
+ def render(self, bound_column, record, value): # pylint: disable=arguments-differ
536
+ related_record = None
537
+ try:
538
+ lookup = self.lookup or get_related_field_for_models(bound_column._table._meta.model, self.model).name
539
+ except AttributeError:
540
+ lookup = None
541
+ if lookup:
542
+ if related_records := getattr(record, f"{lookup}_list", None):
543
+ related_record = related_records[0]
544
+ url = reverse(self.viewname, kwargs=self.view_kwargs)
545
+ if self.url_params:
546
+ url += "?" + urlencode({k: getattr(record, v) for k, v in self.url_params.items()})
547
+ if value > 1:
548
+ return format_html('<a href="{}" class="badge">{}</a>', url, value)
549
+ if related_record is not None:
550
+ return helpers.hyperlinked_object(related_record)
551
+ if value == 1:
552
+ return format_html('<a href="{}" class="badge">{}</a>', url, value)
553
+ return helpers.placeholder(value)
488
554
 
489
555
 
490
556
  class TagColumn(django_tables2.TemplateColumn):
@@ -2,7 +2,7 @@
2
2
  {% load helpers %}
3
3
  {% load render_table from django_tables2 %}
4
4
 
5
- {% block title %}Delete {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
5
+ {% block title %}Delete {{ total_objs_to_delete }} {{ obj_type_plural|bettertitle }}?{% endblock %}
6
6
 
7
7
  {% block content %}
8
8
  <div class="row">
@@ -10,12 +10,14 @@
10
10
  <div class="panel panel-danger">
11
11
  <div class="panel-heading"><strong>Confirm Bulk Deletion</strong></div>
12
12
  <div class="panel-body">
13
- <p><strong>Warning:</strong> The following operation will delete {{ table.rows|length }} {{ obj_type_plural }}. Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.</p>
13
+ <p><strong>Warning:</strong> The following operation will delete {{ total_objs_to_delete }} {{ obj_type_plural }}. {% if not delete_all %}Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.{% endif %}</p>
14
14
  {% block message_extra %}{% endblock %}
15
15
  </div>
16
16
  </div>
17
17
  </div>
18
18
  </div>
19
+
20
+ {% if table %}
19
21
  <div class="row">
20
22
  <div class="col-md-8 col-md-offset-2">
21
23
  <div class="panel panel-default">
@@ -25,15 +27,22 @@
25
27
  </div>
26
28
  </div>
27
29
  </div>
30
+ {% endif %}
31
+
28
32
  <div class="row">
29
33
  <div class="col-md-6 col-md-offset-3">
30
34
  <form action="" method="post" class="form">
35
+
31
36
  {% csrf_token %}
37
+ {% if delete_all %}
38
+ <input type="hidden" name="_all" value="true" />
39
+ {% endif %}
32
40
  {% for field in form.hidden_fields %}
33
41
  {{ field }}
34
42
  {% endfor %}
43
+
35
44
  <div class="text-center">
36
- <button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ table.rows|length }} {{ obj_type_plural }}</button>
45
+ <button type="submit" name="_confirm" class="btn btn-danger">Delete these {{ total_objs_to_delete }} {{ obj_type_plural }}</button>
37
46
  <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
38
47
  </div>
39
48
  </form>
@@ -4,7 +4,7 @@
4
4
  {% load render_table from django_tables2 %}
5
5
 
6
6
  {% block content %}
7
- <h1>{% block title %}Editing {{ table.rows|length }} {{ obj_type_plural|bettertitle }}{% endblock %}</h1>
7
+ <h1>{% block title %}Editing {{ objs_count }} {{ obj_type_plural|bettertitle }}{% endblock %}</h1>
8
8
  {% if form.errors %}
9
9
  <div class="panel panel-danger">
10
10
  <div class="panel-heading"><strong>Errors</strong></div>
@@ -27,6 +27,7 @@
27
27
  {{ field }}
28
28
  {% endfor %}
29
29
  <div class="row">
30
+ {% if table %}
30
31
  <div class="col-md-8">
31
32
  <div class="panel panel-default">
32
33
  <div class="table-responsive">
@@ -34,7 +35,8 @@
34
35
  </div>
35
36
  </div>
36
37
  </div>
37
- <div class="col-md-4">
38
+ {% endif %}
39
+ <div class="{% if table %} col-md-4 {% else %} col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 {% endif %}">
38
40
  <div class="panel panel-default">
39
41
  <div class="panel-heading"><strong>{% block form_title %}Attributes{% endblock %}</strong></div>
40
42
  <div class="panel-body">
@@ -50,7 +50,7 @@
50
50
  </div>
51
51
  </div>
52
52
  <div class="row">
53
- <div class="col-md-6 col-md-offset-3 text-right">
53
+ <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-right">
54
54
  {% block buttons %}
55
55
  {% if editing %}
56
56
  <button type="submit" name="_update" class="btn btn-primary">Update</button>
@@ -20,6 +20,9 @@
20
20
  #main-content {
21
21
  min-height: calc(100vh - 70px);
22
22
  }
23
+ .request-info {
24
+ clear: both; /* Fix description floating issue */
25
+ }
23
26
  </style>
24
27
  {% endblock body %}
25
28
 
@@ -699,13 +699,15 @@ class APIViewTestCases:
699
699
  serializer_class = get_serializer_for_model(self.model)
700
700
  old_serializer = serializer_class(instance, context={"request": None})
701
701
  old_data = old_serializer.data
702
+ # save the pk because .delete() will clear it, making the test below always pass
703
+ orig_pk = instance.pk
702
704
  instance.delete()
703
705
 
704
706
  response = self.client.post(self._get_list_url(), csv_data, content_type="text/csv", **self.header)
705
707
  self.assertHttpStatus(response, status.HTTP_201_CREATED, csv_data)
706
708
  # Note that create via CSV is always treated as a bulk-create, and so the response is always a list of dicts
707
709
  new_instance = self._get_queryset().get(pk=response.data[0]["id"])
708
- self.assertNotEqual(new_instance.pk, instance.pk)
710
+ self.assertNotEqual(new_instance.pk, orig_pk)
709
711
 
710
712
  new_serializer = serializer_class(new_instance, context={"request": None})
711
713
  new_data = new_serializer.data
@@ -5,6 +5,7 @@ from django.contrib.staticfiles.testing import StaticLiveServerTestCase
5
5
  from django.test import override_settings, tag
6
6
  from django.urls import reverse
7
7
  from django.utils.functional import classproperty
8
+ from selenium.webdriver.common.keys import Keys
8
9
  from splinter.browser import Browser
9
10
 
10
11
  from nautobot.core import testing
@@ -74,3 +75,66 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
74
75
 
75
76
  def logout(self):
76
77
  self.browser.visit(f"{self.live_server_url}/logout")
78
+
79
+ def click_navbar_entry(self, parent_menu_name, child_menu_name):
80
+ """
81
+ Helper function to click on a parent menu and child menu in the navigation bar.
82
+ """
83
+
84
+ parent_menu_xpath = f"//*[@id='navbar']//a[@class='dropdown-toggle' and normalize-space()='{parent_menu_name}']"
85
+ parent_menu = self.browser.find_by_xpath(parent_menu_xpath, wait_time=5)
86
+ if not parent_menu["aria-expanded"] == "true":
87
+ parent_menu.click()
88
+ child_menu_xpath = f"{parent_menu_xpath}/following-sibling::ul//li[.//a[normalize-space()='{child_menu_name}']]"
89
+ child_menu = self.browser.find_by_xpath(child_menu_xpath, wait_time=5)
90
+ child_menu.click()
91
+
92
+ # Wait for body element to appear
93
+ self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
94
+
95
+ def click_list_view_add_button(self):
96
+ """
97
+ Helper function to click the "Add" button on a list view.
98
+ """
99
+ add_button = self.browser.find_by_xpath("//a[@id='add-button']", wait_time=5)
100
+ add_button.click()
101
+
102
+ # Wait for body element to appear
103
+ self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
104
+
105
+ def click_edit_form_create_button(self):
106
+ """
107
+ Helper function to click the "Create" button on a form.
108
+ """
109
+ add_button = self.browser.find_by_xpath("//button[@name='_create']", wait_time=5)
110
+ add_button.click()
111
+
112
+ # Wait for body element to appear
113
+ self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=5), "Page failed to load")
114
+
115
+ def fill_select2_field(self, field_name, value):
116
+ """
117
+ Helper function to fill a Select2 single selection field.
118
+ """
119
+ self.browser.find_by_xpath(f"//select[@id='id_{field_name}']//following-sibling::span").click()
120
+ search_box = self.browser.find_by_xpath(
121
+ "//*[@class='select2-search select2-search--dropdown']//input", wait_time=5
122
+ )
123
+ for _ in search_box.first.type(value, slowly=True):
124
+ pass
125
+
126
+ # wait for "searching" to disappear
127
+ self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
128
+ search_box.first.type(Keys.ENTER)
129
+
130
+ def fill_select2_multiselect_field(self, field_name, value):
131
+ """
132
+ Helper function to fill a Select2 multi-selection field.
133
+ """
134
+ search_box = self.browser.find_by_xpath(f"//select[@id='id_{field_name}']//following-sibling::span//input")
135
+ for _ in search_box.first.type(value, slowly=True):
136
+ pass
137
+
138
+ # wait for "searching" to disappear
139
+ self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
140
+ search_box.first.type(Keys.ENTER)