nautobot 2.3.10__py3-none-any.whl → 2.3.12__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 (524) 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/settings.py +26 -0
  5. nautobot/core/settings.yaml +31 -0
  6. nautobot/core/tables.py +88 -22
  7. nautobot/core/templates/generic/object_bulk_destroy.html +12 -3
  8. nautobot/core/templates/generic/object_bulk_update.html +4 -2
  9. nautobot/core/templates/generic/object_create.html +1 -1
  10. nautobot/core/templates/generic/object_notes.html +1 -1
  11. nautobot/core/templates/generic/object_retrieve.html +1 -1
  12. nautobot/core/templates/rest_framework/api.html +3 -0
  13. nautobot/core/testing/api.py +3 -1
  14. nautobot/core/testing/integration.py +64 -0
  15. nautobot/core/testing/views.py +48 -31
  16. nautobot/core/tests/integration/test_app_navbar.py +3 -3
  17. nautobot/core/tests/integration/test_navbar.py +1 -1
  18. nautobot/core/tests/test_csv.py +3 -0
  19. nautobot/core/tests/test_utils.py +25 -5
  20. nautobot/core/utils/lookup.py +35 -0
  21. nautobot/core/views/generic.py +50 -39
  22. nautobot/core/views/mixins.py +97 -43
  23. nautobot/core/views/renderers.py +8 -5
  24. nautobot/dcim/choices.py +6 -0
  25. nautobot/dcim/tables/devices.py +5 -6
  26. nautobot/dcim/templates/dcim/controller_retrieve.html +1 -1
  27. nautobot/dcim/templates/dcim/device/base.html +1 -1
  28. nautobot/dcim/templates/dcim/device_component_add.html +8 -8
  29. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +2 -2
  30. nautobot/dcim/templates/dcim/virtualchassis_edit.html +2 -2
  31. nautobot/dcim/tests/integration/test_create_device.py +86 -0
  32. nautobot/extras/group_sync.py +42 -0
  33. nautobot/extras/models/metadata.py +1 -0
  34. nautobot/extras/models/models.py +1 -1
  35. nautobot/extras/plugins/__init__.py +2 -1
  36. nautobot/extras/tables.py +1 -0
  37. nautobot/extras/templates/extras/inc/job_table.html +1 -1
  38. nautobot/extras/tests/test_relationships.py +1 -0
  39. nautobot/extras/tests/test_views.py +0 -2
  40. nautobot/extras/views.py +1 -2
  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 +6 -13
  47. nautobot/ipam/templates/ipam/ipaddress_assign.html +2 -2
  48. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -1
  49. nautobot/ipam/tests/test_models.py +113 -1
  50. nautobot/ipam/tests/test_views.py +39 -5
  51. nautobot/project-static/docs/404.html +10 -10
  52. nautobot/project-static/docs/additional-features/caching.html +1 -2
  53. nautobot/project-static/docs/additional-features/change-logging.html +1 -2
  54. nautobot/project-static/docs/additional-features/config-contexts.html +1 -2
  55. nautobot/project-static/docs/additional-features/healthcheck.html +1 -2
  56. nautobot/project-static/docs/additional-features/jobs.html +1 -2
  57. nautobot/project-static/docs/additional-features/prometheus-metrics.html +1 -2
  58. nautobot/project-static/docs/administration/celery-queues.html +1 -2
  59. nautobot/project-static/docs/administration/nautobot-server.html +1 -2
  60. nautobot/project-static/docs/administration/nautobot-shell.html +1 -2
  61. nautobot/project-static/docs/administration/permissions.html +1 -2
  62. nautobot/project-static/docs/administration/replicating-nautobot.html +1 -2
  63. nautobot/project-static/docs/apps/index.html +10 -10
  64. nautobot/project-static/docs/apps/migrating-jobs-from-nautobot-v1.html +1 -2
  65. nautobot/project-static/docs/apps/nautobot-apps.html +10 -10
  66. nautobot/project-static/docs/assets/stylesheets/main.6f8fc17f.min.css +1 -0
  67. nautobot/project-static/docs/assets/stylesheets/main.6f8fc17f.min.css.map +1 -0
  68. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +10 -10
  69. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +10 -10
  70. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +10 -10
  71. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +10 -10
  72. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +10 -10
  73. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +10 -10
  74. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +10 -10
  75. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +10 -10
  76. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +10 -10
  77. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +10 -10
  78. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +10 -10
  79. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +10 -10
  80. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +10 -10
  81. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +10 -10
  82. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +10 -10
  83. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +10 -10
  84. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +10 -10
  85. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +141 -16
  86. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +185 -10
  87. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +10 -10
  88. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +10 -10
  89. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +104 -10
  90. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +14 -14
  91. nautobot/project-static/docs/configuration/authentication/ldap.html +1 -2
  92. nautobot/project-static/docs/configuration/authentication/remote.html +1 -2
  93. nautobot/project-static/docs/configuration/authentication/sso.html +1 -2
  94. nautobot/project-static/docs/configuration/index.html +1 -2
  95. nautobot/project-static/docs/configuration/optional-settings.html +1 -2
  96. nautobot/project-static/docs/configuration/required-settings.html +1 -2
  97. nautobot/project-static/docs/core-functionality/circuits.html +1 -2
  98. nautobot/project-static/docs/core-functionality/device-types.html +1 -2
  99. nautobot/project-static/docs/core-functionality/devices.html +1 -2
  100. nautobot/project-static/docs/core-functionality/ipam.html +1 -2
  101. nautobot/project-static/docs/core-functionality/power.html +1 -2
  102. nautobot/project-static/docs/core-functionality/secrets.html +1 -2
  103. nautobot/project-static/docs/core-functionality/services.html +1 -2
  104. nautobot/project-static/docs/core-functionality/sites-and-racks.html +1 -2
  105. nautobot/project-static/docs/core-functionality/tenancy.html +1 -2
  106. nautobot/project-static/docs/core-functionality/virtualization.html +1 -2
  107. nautobot/project-static/docs/core-functionality/vlans.html +1 -2
  108. nautobot/project-static/docs/development/application-registry.html +1 -2
  109. nautobot/project-static/docs/development/apps/api/configuration-view.html +10 -10
  110. nautobot/project-static/docs/development/apps/api/database-backend-config.html +10 -10
  111. nautobot/project-static/docs/development/apps/api/models/django-admin.html +10 -10
  112. nautobot/project-static/docs/development/apps/api/models/global-search.html +10 -10
  113. nautobot/project-static/docs/development/apps/api/models/graphql.html +10 -10
  114. nautobot/project-static/docs/development/apps/api/models/index.html +10 -10
  115. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +10 -10
  116. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +10 -10
  117. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +10 -10
  118. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +10 -10
  119. nautobot/project-static/docs/development/apps/api/platform-features/index.html +10 -10
  120. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +10 -10
  121. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +10 -10
  122. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +10 -10
  123. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +10 -10
  124. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +10 -10
  125. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +10 -10
  126. nautobot/project-static/docs/development/apps/api/prometheus.html +10 -10
  127. nautobot/project-static/docs/development/apps/api/setup.html +10 -10
  128. nautobot/project-static/docs/development/apps/api/testing.html +10 -10
  129. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +10 -10
  130. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +10 -10
  131. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +10 -10
  132. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +10 -10
  133. nautobot/project-static/docs/development/apps/api/ui-extensions/object-detail-views.html +1 -2
  134. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +10 -10
  135. nautobot/project-static/docs/development/apps/api/ui-extensions/tabs.html +1 -2
  136. nautobot/project-static/docs/development/apps/api/views/base-template.html +10 -10
  137. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +10 -10
  138. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +10 -10
  139. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +10 -10
  140. nautobot/project-static/docs/development/apps/api/views/index.html +10 -10
  141. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +10 -10
  142. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +10 -10
  143. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +10 -10
  144. nautobot/project-static/docs/development/apps/api/views/notes.html +10 -10
  145. nautobot/project-static/docs/development/apps/api/views/rest-api.html +10 -10
  146. nautobot/project-static/docs/development/apps/api/views/urls.html +10 -10
  147. nautobot/project-static/docs/development/apps/api/views/view-overrides.html +1 -2
  148. nautobot/project-static/docs/development/apps/index.html +10 -10
  149. nautobot/project-static/docs/development/apps/migration/code-updates.html +10 -10
  150. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +10 -10
  151. nautobot/project-static/docs/development/apps/migration/from-v1.html +10 -10
  152. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +10 -10
  153. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +10 -10
  154. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +10 -10
  155. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +10 -10
  156. nautobot/project-static/docs/development/apps/porting-from-netbox.html +10 -10
  157. nautobot/project-static/docs/development/best-practices.html +1 -2
  158. nautobot/project-static/docs/development/core/application-registry.html +10 -10
  159. nautobot/project-static/docs/development/core/best-practices.html +10 -10
  160. nautobot/project-static/docs/development/core/bootstrap-ui.html +10 -10
  161. nautobot/project-static/docs/development/core/caching.html +10 -10
  162. nautobot/project-static/docs/development/core/controllers.html +10 -10
  163. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +10 -10
  164. nautobot/project-static/docs/development/core/extending-models.html +1 -2
  165. nautobot/project-static/docs/development/core/generic-views.html +10 -10
  166. nautobot/project-static/docs/development/core/getting-started.html +10 -10
  167. nautobot/project-static/docs/development/core/homepage.html +10 -10
  168. nautobot/project-static/docs/development/core/index.html +10 -10
  169. nautobot/project-static/docs/development/core/model-checklist.html +10 -10
  170. nautobot/project-static/docs/development/core/model-features.html +10 -10
  171. nautobot/project-static/docs/development/core/natural-keys.html +10 -10
  172. nautobot/project-static/docs/development/core/navigation-menu.html +10 -10
  173. nautobot/project-static/docs/development/core/react-ui.html +1 -2
  174. nautobot/project-static/docs/development/core/release-checklist.html +10 -10
  175. nautobot/project-static/docs/development/core/role-internals.html +10 -10
  176. nautobot/project-static/docs/development/core/settings.html +10 -10
  177. nautobot/project-static/docs/development/core/style-guide.html +10 -10
  178. nautobot/project-static/docs/development/core/templates.html +10 -10
  179. nautobot/project-static/docs/development/core/testing.html +12 -12
  180. nautobot/project-static/docs/development/core/user-preferences.html +10 -10
  181. nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1 -2
  182. nautobot/project-static/docs/development/extending-models.html +1 -2
  183. nautobot/project-static/docs/development/generic-views.html +1 -2
  184. nautobot/project-static/docs/development/getting-started.html +1 -2
  185. nautobot/project-static/docs/development/homepage.html +1 -2
  186. nautobot/project-static/docs/development/index.html +10 -10
  187. nautobot/project-static/docs/development/jobs/index.html +10 -10
  188. nautobot/project-static/docs/development/jobs/migration/from-v1.html +10 -10
  189. nautobot/project-static/docs/development/model-features.html +1 -2
  190. nautobot/project-static/docs/development/natural-keys.html +1 -2
  191. nautobot/project-static/docs/development/navigation-menu.html +1 -2
  192. nautobot/project-static/docs/development/react-ui.html +1 -2
  193. nautobot/project-static/docs/development/release-checklist.html +1 -2
  194. nautobot/project-static/docs/development/role-internals.html +1 -2
  195. nautobot/project-static/docs/development/style-guide.html +1 -2
  196. nautobot/project-static/docs/development/templates.html +1 -2
  197. nautobot/project-static/docs/development/testing.html +1 -2
  198. nautobot/project-static/docs/development/user-preferences.html +1 -2
  199. nautobot/project-static/docs/docker/index.html +1 -2
  200. nautobot/project-static/docs/index.html +10 -10
  201. nautobot/project-static/docs/installation/centos.html +1 -2
  202. nautobot/project-static/docs/installation/external-authentication.html +1 -2
  203. nautobot/project-static/docs/installation/http-server.html +1 -2
  204. nautobot/project-static/docs/installation/index.html +1 -2
  205. nautobot/project-static/docs/installation/migrating-from-netbox.html +1 -2
  206. nautobot/project-static/docs/installation/migrating-from-postgresql.html +1 -2
  207. nautobot/project-static/docs/installation/nautobot.html +1 -2
  208. nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1 -2
  209. nautobot/project-static/docs/installation/selinux-troubleshooting.html +1 -2
  210. nautobot/project-static/docs/installation/services.html +1 -2
  211. nautobot/project-static/docs/installation/ubuntu.html +1 -2
  212. nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +1 -2
  213. nautobot/project-static/docs/installation/upgrading.html +1 -2
  214. nautobot/project-static/docs/models/circuits/circuit.html +1 -2
  215. nautobot/project-static/docs/models/circuits/circuittermination.html +1 -2
  216. nautobot/project-static/docs/models/circuits/circuittype.html +1 -2
  217. nautobot/project-static/docs/models/circuits/provider.html +1 -2
  218. nautobot/project-static/docs/models/circuits/providernetwork.html +1 -2
  219. nautobot/project-static/docs/models/cloud/cloudaccount.html +1 -2
  220. nautobot/project-static/docs/models/cloud/cloudnetwork.html +1 -2
  221. nautobot/project-static/docs/models/cloud/cloudnetworkprefixassignment.html +1 -2
  222. nautobot/project-static/docs/models/cloud/cloudresourcetype.html +1 -2
  223. nautobot/project-static/docs/models/cloud/cloudservice.html +1 -2
  224. nautobot/project-static/docs/models/cloud/cloudservicenetworkassignment.html +1 -2
  225. nautobot/project-static/docs/models/dcim/cable.html +1 -2
  226. nautobot/project-static/docs/models/dcim/consoleport.html +1 -2
  227. nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1 -2
  228. nautobot/project-static/docs/models/dcim/consoleserverport.html +1 -2
  229. nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1 -2
  230. nautobot/project-static/docs/models/dcim/controller.html +1 -2
  231. nautobot/project-static/docs/models/dcim/controllermanageddevicegroup.html +1 -2
  232. nautobot/project-static/docs/models/dcim/device.html +1 -2
  233. nautobot/project-static/docs/models/dcim/devicebay.html +1 -2
  234. nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1 -2
  235. nautobot/project-static/docs/models/dcim/devicefamily.html +1 -2
  236. nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1 -2
  237. nautobot/project-static/docs/models/dcim/devicetype.html +1 -2
  238. nautobot/project-static/docs/models/dcim/frontport.html +1 -2
  239. nautobot/project-static/docs/models/dcim/frontporttemplate.html +1 -2
  240. nautobot/project-static/docs/models/dcim/interface.html +1 -2
  241. nautobot/project-static/docs/models/dcim/interfacetemplate.html +1 -2
  242. nautobot/project-static/docs/models/dcim/inventoryitem.html +1 -2
  243. nautobot/project-static/docs/models/dcim/location.html +1 -2
  244. nautobot/project-static/docs/models/dcim/locationtype.html +1 -2
  245. nautobot/project-static/docs/models/dcim/manufacturer.html +1 -2
  246. nautobot/project-static/docs/models/dcim/module.html +1 -2
  247. nautobot/project-static/docs/models/dcim/modulebay.html +1 -2
  248. nautobot/project-static/docs/models/dcim/modulebaytemplate.html +1 -2
  249. nautobot/project-static/docs/models/dcim/moduletype.html +1 -2
  250. nautobot/project-static/docs/models/dcim/platform.html +1 -2
  251. nautobot/project-static/docs/models/dcim/powerfeed.html +1 -2
  252. nautobot/project-static/docs/models/dcim/poweroutlet.html +1 -2
  253. nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1 -2
  254. nautobot/project-static/docs/models/dcim/powerpanel.html +1 -2
  255. nautobot/project-static/docs/models/dcim/powerport.html +1 -2
  256. nautobot/project-static/docs/models/dcim/powerporttemplate.html +1 -2
  257. nautobot/project-static/docs/models/dcim/rack.html +1 -2
  258. nautobot/project-static/docs/models/dcim/rackgroup.html +1 -2
  259. nautobot/project-static/docs/models/dcim/rackreservation.html +1 -2
  260. nautobot/project-static/docs/models/dcim/rearport.html +1 -2
  261. nautobot/project-static/docs/models/dcim/rearporttemplate.html +1 -2
  262. nautobot/project-static/docs/models/dcim/softwareimagefile.html +1 -2
  263. nautobot/project-static/docs/models/dcim/softwareversion.html +1 -2
  264. nautobot/project-static/docs/models/dcim/virtualchassis.html +1 -2
  265. nautobot/project-static/docs/models/extras/computedfield.html +1 -2
  266. nautobot/project-static/docs/models/extras/configcontext.html +1 -2
  267. nautobot/project-static/docs/models/extras/configcontextschema.html +1 -2
  268. nautobot/project-static/docs/models/extras/contact.html +1 -2
  269. nautobot/project-static/docs/models/extras/customfield.html +1 -2
  270. nautobot/project-static/docs/models/extras/customlink.html +1 -2
  271. nautobot/project-static/docs/models/extras/dynamicgroup.html +1 -2
  272. nautobot/project-static/docs/models/extras/exporttemplate.html +1 -2
  273. nautobot/project-static/docs/models/extras/gitrepository.html +1 -2
  274. nautobot/project-static/docs/models/extras/jobhook.html +1 -2
  275. nautobot/project-static/docs/models/extras/joblogentry.html +1 -2
  276. nautobot/project-static/docs/models/extras/jobresult.html +1 -2
  277. nautobot/project-static/docs/models/extras/metadatachoice.html +1 -2
  278. nautobot/project-static/docs/models/extras/metadatatype.html +1 -2
  279. nautobot/project-static/docs/models/extras/objectmetadata.html +1 -2
  280. nautobot/project-static/docs/models/extras/role.html +1 -2
  281. nautobot/project-static/docs/models/extras/savedview.html +1 -2
  282. nautobot/project-static/docs/models/extras/secret.html +1 -2
  283. nautobot/project-static/docs/models/extras/secretsgroup.html +1 -2
  284. nautobot/project-static/docs/models/extras/staticgroupassociation.html +1 -2
  285. nautobot/project-static/docs/models/extras/status.html +1 -2
  286. nautobot/project-static/docs/models/extras/team.html +1 -2
  287. nautobot/project-static/docs/models/ipam/ipaddress.html +1 -2
  288. nautobot/project-static/docs/models/ipam/prefix.html +1 -2
  289. nautobot/project-static/docs/models/ipam/rir.html +1 -2
  290. nautobot/project-static/docs/models/ipam/routetarget.html +1 -2
  291. nautobot/project-static/docs/models/ipam/service.html +1 -2
  292. nautobot/project-static/docs/models/ipam/vlan.html +1 -2
  293. nautobot/project-static/docs/models/ipam/vlangroup.html +1 -2
  294. nautobot/project-static/docs/models/ipam/vrf.html +1 -2
  295. nautobot/project-static/docs/models/tenancy/tenant.html +1 -2
  296. nautobot/project-static/docs/models/tenancy/tenantgroup.html +1 -2
  297. nautobot/project-static/docs/models/virtualization/cluster.html +1 -2
  298. nautobot/project-static/docs/models/virtualization/clustergroup.html +1 -2
  299. nautobot/project-static/docs/models/virtualization/clustertype.html +1 -2
  300. nautobot/project-static/docs/models/virtualization/virtualmachine.html +1 -2
  301. nautobot/project-static/docs/models/virtualization/vminterface.html +1 -2
  302. nautobot/project-static/docs/objects.inv +0 -0
  303. nautobot/project-static/docs/overview/application_stack.html +10 -10
  304. nautobot/project-static/docs/overview/design_philosophy.html +10 -10
  305. nautobot/project-static/docs/overview/index.html +1 -2
  306. nautobot/project-static/docs/plugins/development.html +1 -2
  307. nautobot/project-static/docs/plugins/index.html +1 -2
  308. nautobot/project-static/docs/plugins/porting-from-netbox.html +1 -2
  309. nautobot/project-static/docs/release-notes/index.html +15 -15
  310. nautobot/project-static/docs/release-notes/version-1.0.html +10 -10
  311. nautobot/project-static/docs/release-notes/version-1.1.html +10 -10
  312. nautobot/project-static/docs/release-notes/version-1.2.html +10 -10
  313. nautobot/project-static/docs/release-notes/version-1.3.html +10 -10
  314. nautobot/project-static/docs/release-notes/version-1.4.html +10 -10
  315. nautobot/project-static/docs/release-notes/version-1.5.html +10 -10
  316. nautobot/project-static/docs/release-notes/version-1.6.html +10 -10
  317. nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
  318. nautobot/project-static/docs/release-notes/version-2.1.html +10 -10
  319. nautobot/project-static/docs/release-notes/version-2.2.html +10 -10
  320. nautobot/project-static/docs/release-notes/version-2.3.html +491 -182
  321. nautobot/project-static/docs/requirements.txt +2 -2
  322. nautobot/project-static/docs/rest-api/overview.html +1 -2
  323. nautobot/project-static/docs/search/search_index.json +1 -1
  324. nautobot/project-static/docs/sitemap.xml +270 -270
  325. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  326. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +10 -10
  327. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +10 -10
  328. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +65 -12
  329. nautobot/project-static/docs/user-guide/administration/configuration/index.html +10 -10
  330. nautobot/project-static/docs/user-guide/administration/configuration/node-configuration.html +1 -2
  331. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +1 -2
  332. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +10 -10
  333. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +1 -2
  334. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +122 -10
  335. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +10 -10
  336. nautobot/project-static/docs/user-guide/administration/guides/caching.html +1 -2
  337. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +10 -10
  338. nautobot/project-static/docs/user-guide/administration/guides/docker.html +10 -10
  339. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +10 -10
  340. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +1 -2
  341. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +10 -10
  342. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +10 -10
  343. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +10 -10
  344. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +49 -10
  345. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +10 -10
  346. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +10 -10
  347. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +10 -10
  348. nautobot/project-static/docs/user-guide/administration/installation/docker.html +1 -2
  349. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +10 -10
  350. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +1 -2
  351. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +10 -10
  352. nautobot/project-static/docs/user-guide/administration/installation/index.html +10 -10
  353. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +10 -10
  354. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +10 -10
  355. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +1 -2
  356. nautobot/project-static/docs/user-guide/administration/installation/services.html +10 -10
  357. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +1 -2
  358. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +1 -2
  359. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +1 -2
  360. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +10 -10
  361. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +10 -10
  362. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +10 -10
  363. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +10 -10
  364. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +10 -10
  365. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +10 -10
  366. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +10 -10
  367. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +10 -10
  368. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +10 -10
  369. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +10 -10
  370. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +10 -10
  371. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +10 -10
  372. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +10 -10
  373. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +10 -10
  374. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +10 -10
  375. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +10 -10
  376. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +10 -10
  377. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +10 -10
  378. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +10 -10
  379. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +10 -10
  380. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +10 -10
  381. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +10 -10
  382. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +10 -10
  383. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +10 -10
  384. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +10 -10
  385. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +10 -10
  386. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +10 -10
  387. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +10 -10
  388. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +10 -10
  389. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +10 -10
  390. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +10 -10
  391. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +10 -10
  392. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +10 -10
  393. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +10 -10
  394. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +10 -10
  395. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +10 -10
  396. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +10 -10
  397. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +10 -10
  398. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +10 -10
  399. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +10 -10
  400. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +10 -10
  401. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +10 -10
  402. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +10 -10
  403. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +10 -10
  404. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +10 -10
  405. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +10 -10
  406. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +10 -10
  407. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +10 -10
  408. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +10 -10
  409. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +10 -10
  410. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +10 -10
  411. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +10 -10
  412. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +10 -10
  413. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +10 -10
  414. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +10 -10
  415. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +10 -10
  416. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +10 -10
  417. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +10 -10
  418. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +10 -10
  419. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +10 -10
  420. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +10 -10
  421. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +10 -10
  422. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +10 -10
  423. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +10 -10
  424. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +10 -10
  425. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +10 -10
  426. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +10 -10
  427. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +10 -10
  428. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +10 -10
  429. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +10 -10
  430. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +10 -10
  431. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +10 -10
  432. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +10 -10
  433. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +10 -10
  434. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +10 -10
  435. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +10 -10
  436. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +10 -10
  437. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +10 -10
  438. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +10 -10
  439. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +10 -10
  440. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +10 -10
  441. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +10 -10
  442. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +10 -10
  443. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +10 -10
  444. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +10 -10
  445. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +10 -10
  446. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +10 -10
  447. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +10 -10
  448. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +10 -10
  449. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +10 -10
  450. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +10 -10
  451. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +10 -10
  452. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +10 -10
  453. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +10 -10
  454. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +10 -10
  455. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +10 -10
  456. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +10 -10
  457. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +10 -10
  458. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +10 -10
  459. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +10 -10
  460. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +10 -10
  461. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +10 -10
  462. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +10 -10
  463. nautobot/project-static/docs/user-guide/index.html +10 -10
  464. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +10 -10
  465. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +10 -10
  466. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +10 -10
  467. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +10 -10
  468. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +10 -10
  469. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +10 -10
  470. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +10 -10
  471. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +10 -10
  472. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +10 -10
  473. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +10 -10
  474. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +10 -10
  475. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +10 -10
  476. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +10 -10
  477. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +10 -10
  478. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +10 -10
  479. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +10 -10
  480. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +10 -10
  481. nautobot/project-static/docs/user-guide/platform-functionality/note.html +10 -10
  482. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +10 -10
  483. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +10 -10
  484. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +10 -10
  485. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +10 -10
  486. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +10 -10
  487. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +10 -10
  488. nautobot/project-static/docs/user-guide/platform-functionality/role.html +10 -10
  489. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +10 -10
  490. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +10 -10
  491. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +10 -10
  492. nautobot/project-static/docs/user-guide/platform-functionality/status.html +10 -10
  493. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +10 -10
  494. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +10 -10
  495. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +10 -10
  496. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +10 -10
  497. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +10 -10
  498. nautobot/project-static/docs/user-guides/custom-fields.html +1 -2
  499. nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1 -2
  500. nautobot/project-static/docs/user-guides/getting-started/index.html +1 -2
  501. nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1 -2
  502. nautobot/project-static/docs/user-guides/getting-started/ipam.html +1 -2
  503. nautobot/project-static/docs/user-guides/getting-started/platforms.html +1 -2
  504. nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1 -2
  505. nautobot/project-static/docs/user-guides/getting-started/tenants.html +1 -2
  506. nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1 -2
  507. nautobot/project-static/docs/user-guides/git-data-source.html +1 -2
  508. nautobot/project-static/docs/user-guides/graphql.html +1 -2
  509. nautobot/project-static/docs/user-guides/ip-address-merge-tool.html +1 -2
  510. nautobot/project-static/docs/user-guides/relationships.html +1 -2
  511. nautobot/project-static/docs/user-guides/s3-django-storage.html +1 -2
  512. nautobot/project-static/js/forms.js +10 -0
  513. nautobot/virtualization/forms.py +24 -0
  514. nautobot/virtualization/templates/virtualization/vminterface.html +4 -0
  515. nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
  516. nautobot/virtualization/tests/test_views.py +7 -2
  517. {nautobot-2.3.10.dist-info → nautobot-2.3.12.dist-info}/METADATA +2 -2
  518. {nautobot-2.3.10.dist-info → nautobot-2.3.12.dist-info}/RECORD +522 -520
  519. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css +0 -1
  520. nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css.map +0 -1
  521. {nautobot-2.3.10.dist-info → nautobot-2.3.12.dist-info}/LICENSE.txt +0 -0
  522. {nautobot-2.3.10.dist-info → nautobot-2.3.12.dist-info}/NOTICE +0 -0
  523. {nautobot-2.3.10.dist-info → nautobot-2.3.12.dist-info}/WHEEL +0 -0
  524. {nautobot-2.3.10.dist-info → nautobot-2.3.12.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import re
2
3
  from typing import Optional, Sequence
3
4
  from unittest import skipIf
@@ -792,11 +793,16 @@ class ViewTestCases:
792
793
  response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
793
794
  self.assertHttpStatus(response, 200)
794
795
  content = utils.extract_page_body(response.content.decode(response.charset))
796
+ # There should be only one row in the table
797
+ self.assertIn('<tr class="even', content)
798
+ self.assertNotIn('<tr class="odd', content)
795
799
  if hasattr(self.model, "name"):
796
800
  self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
797
801
  self.assertNotRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
798
- if instance1.get_absolute_url() in content:
799
- self.assertNotIn(instance2.get_absolute_url(), content, msg=content)
802
+ with contextlib.suppress(AttributeError):
803
+ # Some models, such as ObjectMetadata, don't have a detail URL
804
+ if instance1.get_absolute_url() in content:
805
+ self.assertNotIn(instance2.get_absolute_url(), content, msg=content)
800
806
 
801
807
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"], STRICT_FILTERING=True)
802
808
  def test_list_objects_unknown_filter_strict_filtering(self):
@@ -833,11 +839,16 @@ class ViewTestCases:
833
839
  content = utils.extract_page_body(response.content.decode(response.charset))
834
840
  self.assertNotIn("Unknown filter field", content, msg=content)
835
841
  self.assertIn("None", content, msg=content)
842
+ # There should be at least two rows in the table
843
+ self.assertIn('<tr class="even', content)
844
+ self.assertIn('<tr class="odd', content)
836
845
  if hasattr(self.model, "name"):
837
846
  self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
838
847
  self.assertRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
839
- if instance1.get_absolute_url() in content:
840
- self.assertIn(instance2.get_absolute_url(), content, msg=content)
848
+ with contextlib.suppress(AttributeError):
849
+ # Some models, such as ObjectMetadata, don't have a detail URL
850
+ if instance1.get_absolute_url() in content:
851
+ self.assertIn(instance2.get_absolute_url(), content, msg=content)
841
852
 
842
853
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
843
854
  def test_list_objects_without_permission(self):
@@ -873,6 +884,8 @@ class ViewTestCases:
873
884
  response_body,
874
885
  )
875
886
 
887
+ return response
888
+
876
889
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
877
890
  def test_list_objects_with_constrained_permission(self):
878
891
  instance1, instance2 = self._get_queryset().all()[:2]
@@ -1112,13 +1125,10 @@ class ViewTestCases:
1112
1125
 
1113
1126
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1114
1127
  def test_bulk_edit_form_contains_all_pks(self):
1115
- # We are testing the intermediary step of bulk_edit with pagination applied.
1128
+ # We are testing the intermediary step of all bulk_edit.
1116
1129
  # i.e. "_all" passed in the form.
1117
1130
  pk_list = self._get_queryset().values_list("pk", flat=True)
1118
- # We only pass in one pk to test the functionality of "_all"
1119
- # which should grab all instance pks regardless of "pk"
1120
1131
  selected_data = {
1121
- "pk": pk_list[:1],
1122
1132
  "_all": "on",
1123
1133
  }
1124
1134
  # Assign model-level permission
@@ -1133,13 +1143,19 @@ class ViewTestCases:
1133
1143
  # after pressing Edit Selected button.
1134
1144
  self.assertHttpStatus(response, 200)
1135
1145
  response_body = utils.extract_page_body(response.content.decode(response.charset))
1146
+ # Assert the table which shows all the selected objects is not part of the html body in edit all case
1147
+ self.assertNotIn('<table class="table table-hover table-headings">', response_body)
1136
1148
  # Check if all the pks are passed into the BulkEditForm/BulkUpdateForm
1137
1149
  for pk in pk_list:
1138
- self.assertIn(f'<input type="hidden" name="pk" value="{pk}"', response_body)
1150
+ self.assertNotIn(str(pk), response_body)
1151
+ self.assertInHTML(
1152
+ '<input type="hidden" name="_all" value="True" class="form-control" required="required" placeholder="None" id="id__all">',
1153
+ response_body,
1154
+ )
1139
1155
 
1140
1156
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1141
1157
  def test_bulk_edit_form_contains_all_filtered(self):
1142
- # We are testing the intermediary step of bulk_edit with pagination applied and additional filter.
1158
+ # We are testing the intermediary step of bulk editing all filtered objects.
1143
1159
  # i.e. "_all" passed in the form and filter using query params.
1144
1160
  self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
1145
1161
 
@@ -1155,7 +1171,6 @@ class ViewTestCases:
1155
1171
 
1156
1172
  # Open bulk update form with first two objects
1157
1173
  selected_data = {
1158
- "pk": third_pk, # This is ignored when filtering with "_all"
1159
1174
  "_all": "on",
1160
1175
  **post_data,
1161
1176
  }
@@ -1164,12 +1179,15 @@ class ViewTestCases:
1164
1179
  # Expect a 200 status cause we are only rendering the bulk edit table after pressing Edit Selected button.
1165
1180
  self.assertHttpStatus(response, 200)
1166
1181
  response_body = utils.extract_page_body(response.content.decode(response.charset))
1167
- # Check if the first and second pk is passed into the form.
1168
- self.assertIn(f'<input type="hidden" name="pk" value="{first_pk}"', response_body)
1169
- self.assertIn(f'<input type="hidden" name="pk" value="{second_pk}"', response_body)
1182
+ # Check if all pks is not part of the html.
1183
+ self.assertNotIn(str(first_pk), response_body)
1184
+ self.assertNotIn(str(second_pk), response_body)
1185
+ self.assertNotIn(str(third_pk), response_body)
1170
1186
  self.assertIn("Editing 2 ", response_body)
1171
- # Check if the third pk is not passed into the form.
1172
- self.assertNotIn(f'<input type="hidden" name="pk" value="{third_pk}"', response_body)
1187
+ self.assertInHTML(
1188
+ '<input type="hidden" name="_all" value="True" class="form-control" required="required" placeholder="None" id="id__all">',
1189
+ response_body,
1190
+ )
1173
1191
 
1174
1192
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1175
1193
  def test_bulk_edit_objects_with_constrained_permission(self):
@@ -1271,14 +1289,10 @@ class ViewTestCases:
1271
1289
  self.assertEqual(self._get_queryset().count(), initial_count - len(pk_list))
1272
1290
 
1273
1291
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1274
- def test_bulk_delete_form_contains_all_pks(self):
1275
- # We are testing the intermediary step of bulk_delete with pagination applied.
1292
+ def test_bulk_delete_form_contains_all_objects(self):
1293
+ # We are testing the intermediary step of bulk_delete all objects.
1276
1294
  # i.e. "_all" passed in the form.
1277
- pk_list = self._get_queryset().values_list("pk", flat=True)
1278
- # We only pass in one pk to test the functionality of "_all"
1279
- # which should grab all instance pks regardless of "pks".
1280
1295
  selected_data = {
1281
- "pk": pk_list[:1],
1282
1296
  "confirm": True,
1283
1297
  "_all": "on",
1284
1298
  }
@@ -1293,13 +1307,16 @@ class ViewTestCases:
1293
1307
  response = self.client.post(self._get_url("bulk_delete"), selected_data)
1294
1308
  self.assertHttpStatus(response, 200)
1295
1309
  response_body = utils.extract_page_body(response.content.decode(response.charset))
1296
- # Check if all the pks are passed into the BulkDeleteForm/BulkDestroyForm
1297
- for pk in pk_list:
1298
- self.assertIn(f'<input type="hidden" name="pk" value="{pk}"', response_body)
1310
+ # Assert the table which shows all the selected objects is not part of the html body in delete all case
1311
+ self.assertNotIn('<table class="table table-hover table-headings">', response_body)
1312
+ # Assert none of the hidden input fields for each of the pks that would be deleted is part of the html body
1313
+ for pk in self._get_queryset().values_list("pk", flat=True):
1314
+ self.assertNotIn(str(pk), response_body)
1315
+ self.assertInHTML('<input type="hidden" name="_all" value="true" />', response_body)
1299
1316
 
1300
1317
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1301
1318
  def test_bulk_delete_form_contains_all_filtered(self):
1302
- # We are testing the intermediary step of bulk_delete with pagination applied and additional filter.
1319
+ # We are testing the intermediary step of bulk_delete all with additional filter.
1303
1320
  # i.e. "_all" passed in the form and filter using query params.
1304
1321
  self.add_permissions(f"{self.model._meta.app_label}.delete_{self.model._meta.model_name}")
1305
1322
 
@@ -1321,12 +1338,12 @@ class ViewTestCases:
1321
1338
  # Expect a 200 status cause we are only rendering the bulk delete table after pressing Delete Selected button.
1322
1339
  self.assertHttpStatus(response, 200)
1323
1340
  response_body = utils.extract_page_body(response.content.decode(response.charset))
1324
- # Check if the first and second pk is passed into the form.
1325
- self.assertIn(f'<input type="hidden" name="pk" value="{first_pk}"', response_body)
1326
- self.assertIn(f'<input type="hidden" name="pk" value="{second_pk}"', response_body)
1341
+ # Check if all pks is not part of the html.
1342
+ self.assertNotIn(str(first_pk), response_body)
1343
+ self.assertNotIn(str(second_pk), response_body)
1344
+ self.assertNotIn(str(third_pk), response_body)
1327
1345
  self.assertIn("<strong>Warning:</strong> The following operation will delete 2 ", response_body)
1328
- # Check if the third pk is not passed into the form.
1329
- self.assertNotIn(f'<input type="hidden" name="pk" value="{third_pk}"', response_body)
1346
+ self.assertInHTML('<input type="hidden" name="_all" value="true" />', response_body)
1330
1347
 
1331
1348
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1332
1349
  def test_bulk_delete_objects_with_constrained_permission(self):
@@ -61,7 +61,7 @@ class AppNavBarTestCase(SeleniumTestCase):
61
61
  tab_xpath = "//*[@id='navbar']//span[normalize-space()='Example Menu']/.."
62
62
  tab = self.browser.find_by_xpath(tab_xpath)
63
63
  tab.click()
64
- self.assertTrue(bool(tab["aria-expanded"]))
64
+ self.assertEqual(tab["aria-expanded"], "true")
65
65
 
66
66
  group = tab.find_by_xpath(f"{tab_xpath}/following-sibling::ul//li[normalize-space()='Example Group 1']")
67
67
 
@@ -82,7 +82,7 @@ class AppNavBarTestCase(SeleniumTestCase):
82
82
  tab_xpath = "//*[@id='navbar']//*[normalize-space()='Circuits']"
83
83
  tab = self.browser.find_by_xpath(tab_xpath)
84
84
  tab.click()
85
- self.assertTrue(bool(tab["aria-expanded"]))
85
+ self.assertEqual(tab["aria-expanded"], "true")
86
86
 
87
87
  for group_name, items in self.navbar["Circuits"].items():
88
88
  group = tab.find_by_xpath(f"{tab_xpath}/following-sibling::ul//li[normalize-space()='{group_name}']")
@@ -114,7 +114,7 @@ class AppNavBarTestCase(SeleniumTestCase):
114
114
  tab_xpath = "//*[@id='navbar']//*[normalize-space()='Apps']"
115
115
  tab = self.browser.find_by_xpath(tab_xpath)
116
116
  tab.click()
117
- self.assertTrue(bool(tab["aria-expanded"]))
117
+ self.assertEqual(tab["aria-expanded"], "true")
118
118
 
119
119
  for group_name, items in self.navbar["Apps"].items():
120
120
  group = tab.find_by_xpath(f"{tab_xpath}/following-sibling::ul//li[normalize-space()='{group_name}']")
@@ -60,7 +60,7 @@ class NavBarTestCase(SeleniumTestCase):
60
60
  tab_xpath = f"//*[@id='navbar']//span[normalize-space()='{tab_name}']/.."
61
61
  tab = self.browser.find_by_xpath(tab_xpath)
62
62
  tab.click()
63
- self.assertTrue(bool(tab["aria-expanded"]))
63
+ self.assertEqual(tab["aria-expanded"], "true")
64
64
 
65
65
  for group_name, items in groups.items():
66
66
  # Append onto tab xpath with group name search
@@ -267,6 +267,9 @@ class CSVParsingRelatedTestCase(TestCase):
267
267
  url = reverse("dcim:device_import")
268
268
  response = self.client.post(url, data)
269
269
  self.assertEqual(response.status_code, 200)
270
+ # uploading the CSV always returns a 200 code with a page with an error message on it
271
+ # ensure we don't have that error message
272
+ self.assertNotIn("FORM-ERROR", response.content.decode(response.charset))
270
273
  self.assertEqual(Device.objects.count(), 4)
271
274
 
272
275
  # Assert TestDevice3 got created with the right fields
@@ -21,6 +21,7 @@ from nautobot.extras import models as extras_models, utils as extras_utils
21
21
  from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
22
22
  from nautobot.extras.models import ObjectChange
23
23
  from nautobot.extras.registry import registry
24
+ from nautobot.ipam import models as ipam_models
24
25
 
25
26
  from example_app.models import ExampleModel
26
27
 
@@ -164,7 +165,7 @@ class GetFooForModelTest(TestCase):
164
165
 
165
166
  def test_get_filterset_for_model(self):
166
167
  """
167
- Test the util function `get_filterset_for_model` returns the appropriate FilterSet, if model (as dotted string or class) provided.
168
+ Test that `get_filterset_for_model` returns the right FilterSet for various inputs.
168
169
  """
169
170
  self.assertEqual(lookup.get_filterset_for_model("dcim.device"), dcim_filters.DeviceFilterSet)
170
171
  self.assertEqual(lookup.get_filterset_for_model(dcim_models.Device), dcim_filters.DeviceFilterSet)
@@ -173,7 +174,7 @@ class GetFooForModelTest(TestCase):
173
174
 
174
175
  def test_get_form_for_model(self):
175
176
  """
176
- Test the util function `get_form_for_model` returns the appropriate Form, if form type and model (as dotted string or class) provided.
177
+ Test that `get_form_for_model` returns the right Form for various inputs.
177
178
  """
178
179
  self.assertEqual(lookup.get_form_for_model("dcim.device", "Filter"), dcim_forms.DeviceFilterForm)
179
180
  self.assertEqual(lookup.get_form_for_model(dcim_models.Device, "Filter"), dcim_forms.DeviceFilterForm)
@@ -184,9 +185,28 @@ class GetFooForModelTest(TestCase):
184
185
  self.assertEqual(lookup.get_form_for_model("dcim.location"), dcim_forms.LocationForm)
185
186
  self.assertEqual(lookup.get_form_for_model(dcim_models.Location), dcim_forms.LocationForm)
186
187
 
188
+ def test_get_related_field_for_models(self):
189
+ """
190
+ Test that `get_related_field_for_models` returns the appropriate field for various inputs.
191
+ """
192
+ # No direct relation found
193
+ self.assertIsNone(lookup.get_related_field_for_models(dcim_models.Device, dcim_models.LocationType))
194
+ # ForeignKey and reverse
195
+ self.assertEqual(lookup.get_related_field_for_models(dcim_models.Device, dcim_models.Location).name, "location")
196
+ self.assertEqual(lookup.get_related_field_for_models(dcim_models.Location, dcim_models.Device).name, "devices")
197
+ # ManyToMany and reverse
198
+ self.assertEqual(
199
+ lookup.get_related_field_for_models(ipam_models.Prefix, dcim_models.Location).name, "locations"
200
+ )
201
+ self.assertEqual(lookup.get_related_field_for_models(dcim_models.Location, ipam_models.Prefix).name, "prefixes")
202
+ # Multiple candidate fields
203
+ with self.assertRaises(AttributeError):
204
+ # both primary_ip4 and primary_ip6 are candidates
205
+ lookup.get_related_field_for_models(dcim_models.Device, ipam_models.IPAddress)
206
+
187
207
  def test_get_route_for_model(self):
188
208
  """
189
- Test the util function `get_route_for_model` returns the appropriate URL route name, if model (as dotted string or class) provided.
209
+ Test that `get_route_for_model` returns the appropriate URL route name for various inputs.
190
210
  """
191
211
  # UI
192
212
  self.assertEqual(lookup.get_route_for_model("dcim.device", "list"), "dcim:device_list")
@@ -221,7 +241,7 @@ class GetFooForModelTest(TestCase):
221
241
 
222
242
  def test_get_table_for_model(self):
223
243
  """
224
- Test the util function `get_table_for_model` returns the appropriate Table, if model (as dotted string or class) provided.
244
+ Test that `get_table_for_model` returns the appropriate Table for various inputs.
225
245
  """
226
246
  self.assertEqual(lookup.get_table_for_model("dcim.device"), tables.DeviceTable)
227
247
  self.assertEqual(lookup.get_table_for_model(dcim_models.Device), tables.DeviceTable)
@@ -237,7 +257,7 @@ class GetFooForModelTest(TestCase):
237
257
 
238
258
  def test_get_model_for_view_name(self):
239
259
  """
240
- Test the util function `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
260
+ Test that `get_model_for_view_name` returns the appropriate Model, if the colon separated view name provided.
241
261
  """
242
262
  with self.subTest("Test core view."):
243
263
  self.assertEqual(lookup.get_model_for_view_name("dcim:device_list"), dcim_models.Device)
@@ -177,6 +177,41 @@ def get_form_for_model(model, form_prefix=""):
177
177
  return get_related_class_for_model(model, module_name="forms", object_suffix=object_suffix)
178
178
 
179
179
 
180
+ def get_related_field_for_models(from_model, to_model):
181
+ """
182
+ Find the field on `from_model` that is a relation to `to_model`.
183
+
184
+ If no such field is found, returns None.
185
+ If more than one such field is found, raises an AttributeError.
186
+
187
+ Args:
188
+ from_model (BaseModel): The model class that should contain the relevant field or relation.
189
+ to_model (BaseModel): The model class that we're looking for as the destination.
190
+
191
+ Examples:
192
+ >>> get_related_field_for_models(Device, Location)
193
+ <django.db.models.fields.related.ForeignKey: location>
194
+ >>> get_related_field_for_models(Location, Device)
195
+ <ManyToOneRel: dcim.device>
196
+ >>> get_related_field_for_models(Prefix, Location)
197
+ <django.db.models.fields.related.ManyToManyField: locations>
198
+ >>> get_related_field_for_models(Location, Prefix)
199
+ <ManyToManyRel: ipam.prefix>
200
+ >>> get_related_field_for_models(Device, IPAddress)
201
+ AttributeError: Device has more than one relation to IPAddress: primary_ip4, primary_ip6
202
+ """
203
+ matching_field = None
204
+ for field in from_model._meta.get_fields():
205
+ if hasattr(field, "remote_field") and field.remote_field and field.remote_field.model == to_model:
206
+ if matching_field is not None:
207
+ raise AttributeError(
208
+ f"{from_model.__name__} has more than one relation to {to_model.__name__}: "
209
+ f"{matching_field.name}, {field.name}"
210
+ )
211
+ matching_field = field
212
+ return matching_field
213
+
214
+
180
215
  def get_table_for_model(model):
181
216
  """Return the `Table` class associated with a given `model`.
182
217
 
@@ -46,7 +46,7 @@ from nautobot.core.utils.requests import (
46
46
  get_filterable_params_from_filter_params,
47
47
  normalize_querydict,
48
48
  )
49
- from nautobot.core.views.mixins import GetReturnURLMixin, ObjectPermissionRequiredMixin
49
+ from nautobot.core.views.mixins import EditAndDeleteAllModelMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin
50
50
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
51
51
  from nautobot.core.views.utils import (
52
52
  check_filter_for_display,
@@ -979,7 +979,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): #
979
979
  )
980
980
 
981
981
 
982
- class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
982
+ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDeleteAllModelMixin, View):
983
983
  """
984
984
  Edit objects in bulk.
985
985
 
@@ -1013,18 +1013,18 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1013
1013
  def post(self, request, **kwargs):
1014
1014
  logger = logging.getLogger(__name__ + ".BulkEditView")
1015
1015
  model = self.queryset.model
1016
+ edit_all = request.POST.get("_all")
1016
1017
 
1017
1018
  # If we are editing *all* objects in the queryset, replace the PK list with all matched objects.
1018
- if request.POST.get("_all"):
1019
- if self.filterset is not None:
1020
- pk_list = list(self.filterset(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
1021
- else:
1022
- pk_list = list(model.objects.all().values_list("pk", flat=True))
1019
+ if edit_all:
1020
+ pk_list = []
1021
+ queryset = self._get_bulk_edit_delete_all_queryset(request)
1023
1022
  else:
1024
1023
  pk_list = request.POST.getlist("pk")
1024
+ queryset = self.queryset.filter(pk__in=pk_list)
1025
1025
 
1026
1026
  if "_apply" in request.POST:
1027
- form = self.form(model, request.POST)
1027
+ form = self.form(model, request.POST, edit_all=edit_all)
1028
1028
  restrict_form_fields(form, request.user)
1029
1029
 
1030
1030
  if form.is_valid():
@@ -1041,7 +1041,8 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1041
1041
  try:
1042
1042
  with deferred_change_logging_for_bulk_operation():
1043
1043
  updated_objects = []
1044
- for obj in self.queryset.filter(pk__in=form.cleaned_data["pk"]):
1044
+ queryset = queryset if edit_all else queryset.filter(pk__in=form.cleaned_data["pk"])
1045
+ for obj in queryset:
1045
1046
  obj = self.alter_obj(obj, request, [], kwargs)
1046
1047
 
1047
1048
  # Update standard fields. If a field is listed in _nullify, delete its value.
@@ -1130,23 +1131,26 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1130
1131
  elif "device_type" in request.GET:
1131
1132
  initial_data["device_type"] = request.GET.get("device_type")
1132
1133
 
1133
- form = self.form(model, initial=initial_data)
1134
+ form = self.form(model, initial=initial_data, edit_all=edit_all)
1134
1135
  restrict_form_fields(form, request.user)
1135
1136
 
1136
1137
  # Retrieve objects being edited
1137
- table = self.table(self.queryset.filter(pk__in=pk_list), orderable=False)
1138
- if not table.rows:
1139
- messages.warning(request, f"No {model._meta.verbose_name_plural} were selected.")
1140
- return redirect(self.get_return_url(request))
1141
- # Hide actions column if present
1142
- if "actions" in table.columns:
1143
- table.columns.hide("actions")
1138
+ table = None
1139
+ if not edit_all:
1140
+ table = self.table(queryset, orderable=False)
1141
+ if not table.rows:
1142
+ messages.warning(request, f"No {model._meta.verbose_name_plural} were selected.")
1143
+ return redirect(self.get_return_url(request))
1144
+ # Hide actions column if present
1145
+ if "actions" in table.columns:
1146
+ table.columns.hide("actions")
1144
1147
 
1145
1148
  context = {
1146
1149
  "form": form,
1147
1150
  "table": table,
1148
1151
  "obj_type_plural": model._meta.verbose_name_plural,
1149
1152
  "return_url": self.get_return_url(request),
1153
+ "objs_count": queryset.count(),
1150
1154
  }
1151
1155
  context.update(self.extra_context())
1152
1156
  return render(request, self.template_name, context)
@@ -1255,7 +1259,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1255
1259
  return ""
1256
1260
 
1257
1261
 
1258
- class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1262
+ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, EditAndDeleteAllModelMixin, View):
1259
1263
  """
1260
1264
  Delete objects in bulk.
1261
1265
 
@@ -1278,18 +1282,37 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1278
1282
  def get(self, request):
1279
1283
  return redirect(self.get_return_url(request))
1280
1284
 
1281
- def post(self, request, **kwargs):
1285
+ def _perform_delete_operation(self, request, queryset, model):
1282
1286
  logger = logging.getLogger(__name__ + ".BulkDeleteView")
1287
+ self.perform_pre_delete(request, queryset)
1288
+ try:
1289
+ _, deleted_info = bulk_delete_with_bulk_change_logging(queryset)
1290
+ deleted_count = deleted_info[model._meta.label]
1291
+ except ProtectedError as e:
1292
+ logger.info("Caught ProtectedError while attempting to delete objects")
1293
+ handle_protectederror(queryset, request, e)
1294
+ return redirect(self.get_return_url(request))
1295
+ msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
1296
+ logger.info(msg)
1297
+ messages.success(request, msg)
1298
+ return redirect(self.get_return_url(request))
1299
+
1300
+ def post(self, request, **kwargs):
1301
+ logger = logging.getLogger(f"{__name__}.BulkDeleteView")
1283
1302
  model = self.queryset.model
1284
1303
 
1285
1304
  # Are we deleting *all* objects in the queryset or just a selected subset?
1286
1305
  if request.POST.get("_all"):
1287
- if self.filterset is not None:
1288
- pk_list = list(self.filterset(request.GET, model.objects.only("pk")).qs.values_list("pk", flat=True))
1289
- else:
1290
- pk_list = list(model.objects.all().values_list("pk", flat=True))
1291
- else:
1292
- pk_list = request.POST.getlist("pk")
1306
+ queryset = self._get_bulk_edit_delete_all_queryset(request)
1307
+
1308
+ if "_confirm" in request.POST:
1309
+ return self._perform_delete_operation(request, queryset, model)
1310
+
1311
+ context = self._bulk_delete_all_context(request, queryset)
1312
+ context.update(self.extra_context())
1313
+ return render(request, self.template_name, context)
1314
+
1315
+ pk_list = request.POST.getlist("pk")
1293
1316
 
1294
1317
  form_cls = self.get_form()
1295
1318
 
@@ -1300,20 +1323,7 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1300
1323
 
1301
1324
  # Delete objects
1302
1325
  queryset = self.queryset.filter(pk__in=pk_list)
1303
-
1304
- self.perform_pre_delete(request, queryset)
1305
- try:
1306
- _, deleted_info = bulk_delete_with_bulk_change_logging(queryset)
1307
- deleted_count = deleted_info[model._meta.label]
1308
- except ProtectedError as e:
1309
- logger.info("Caught ProtectedError while attempting to delete objects")
1310
- handle_protectederror(queryset, request, e)
1311
- return redirect(self.get_return_url(request))
1312
- msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
1313
- logger.info(msg)
1314
- messages.success(request, msg)
1315
- return redirect(self.get_return_url(request))
1316
-
1326
+ return self._perform_delete_operation(request, queryset, model)
1317
1327
  else:
1318
1328
  logger.debug("Form validation failed")
1319
1329
 
@@ -1342,6 +1352,7 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1342
1352
  "obj_type_plural": model._meta.verbose_name_plural,
1343
1353
  "table": table,
1344
1354
  "return_url": self.get_return_url(request),
1355
+ "total_objs_to_delete": len(table.rows),
1345
1356
  }
1346
1357
  context.update(self.extra_context())
1347
1358
  return render(request, self.template_name, context)