nautobot 2.3.6__py3-none-any.whl → 2.3.7__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 (315) hide show
  1. nautobot/__init__.py +4 -2
  2. nautobot/circuits/tests/test_views.py +4 -5
  3. nautobot/core/api/views.py +15 -3
  4. nautobot/core/templates/inc/javascript.html +2 -0
  5. nautobot/core/templates/inc/nav_menu.html +0 -251
  6. nautobot/core/testing/mixins.py +59 -2
  7. nautobot/core/testing/views.py +45 -61
  8. nautobot/core/tests/runner.py +6 -3
  9. nautobot/core/tests/test_paginator.py +4 -3
  10. nautobot/core/tests/test_views.py +39 -56
  11. nautobot/core/views/__init__.py +27 -11
  12. nautobot/dcim/tests/test_views.py +26 -67
  13. nautobot/extras/datasources/git.py +6 -1
  14. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +3 -0
  15. nautobot/extras/migrations/0116_fix_dynamic_group_group_type_data_migration.py +16 -0
  16. nautobot/extras/tests/test_customfields.py +9 -16
  17. nautobot/extras/tests/test_dynamicgroups.py +116 -0
  18. nautobot/extras/tests/test_plugins.py +4 -6
  19. nautobot/extras/tests/test_utils.py +5 -0
  20. nautobot/extras/tests/test_views.py +61 -159
  21. nautobot/extras/utils.py +50 -11
  22. nautobot/ipam/tests/test_api.py +18 -12
  23. nautobot/ipam/tests/test_views.py +6 -15
  24. nautobot/project-static/docs/404.html +3 -3
  25. nautobot/project-static/docs/apps/index.html +3 -3
  26. nautobot/project-static/docs/apps/nautobot-apps.html +3 -3
  27. nautobot/project-static/docs/assets/javascripts/bundle.525ec568.min.js +16 -0
  28. nautobot/project-static/docs/assets/javascripts/{bundle.56dfad97.min.js.map → bundle.525ec568.min.js.map} +4 -4
  29. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +1 -0
  30. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +1 -0
  31. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +3 -3
  32. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +3 -3
  33. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3 -3
  34. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +3 -3
  35. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +3 -3
  36. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +3 -3
  37. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +3 -3
  38. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +3 -3
  39. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +3 -3
  40. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +3 -3
  41. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +3 -3
  42. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +3 -3
  43. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
  44. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
  45. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
  46. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
  47. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
  48. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
  49. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +124 -3
  50. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +3 -3
  51. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +3 -3
  52. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +3 -3
  53. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3 -3
  54. nautobot/project-static/docs/development/apps/api/configuration-view.html +3 -3
  55. nautobot/project-static/docs/development/apps/api/database-backend-config.html +3 -3
  56. nautobot/project-static/docs/development/apps/api/models/django-admin.html +3 -3
  57. nautobot/project-static/docs/development/apps/api/models/global-search.html +3 -3
  58. nautobot/project-static/docs/development/apps/api/models/graphql.html +3 -3
  59. nautobot/project-static/docs/development/apps/api/models/index.html +3 -3
  60. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
  61. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
  62. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +3 -3
  63. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
  64. nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
  65. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
  66. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
  67. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
  68. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
  69. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
  70. nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
  71. nautobot/project-static/docs/development/apps/api/setup.html +3 -3
  72. nautobot/project-static/docs/development/apps/api/testing.html +3 -3
  73. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
  74. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
  75. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
  76. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
  77. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
  78. nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
  79. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
  80. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
  81. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
  82. nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
  83. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
  84. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
  85. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
  86. nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
  87. nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
  88. nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
  89. nautobot/project-static/docs/development/apps/index.html +3 -3
  90. nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
  91. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
  92. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  93. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
  94. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
  95. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
  96. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
  97. nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
  98. nautobot/project-static/docs/development/core/application-registry.html +3 -3
  99. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  100. nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
  101. nautobot/project-static/docs/development/core/caching.html +3 -3
  102. nautobot/project-static/docs/development/core/controllers.html +3 -3
  103. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
  104. nautobot/project-static/docs/development/core/generic-views.html +3 -3
  105. nautobot/project-static/docs/development/core/getting-started.html +3 -3
  106. nautobot/project-static/docs/development/core/homepage.html +3 -3
  107. nautobot/project-static/docs/development/core/index.html +3 -3
  108. nautobot/project-static/docs/development/core/model-checklist.html +3 -3
  109. nautobot/project-static/docs/development/core/model-features.html +3 -3
  110. nautobot/project-static/docs/development/core/natural-keys.html +3 -3
  111. nautobot/project-static/docs/development/core/navigation-menu.html +3 -3
  112. nautobot/project-static/docs/development/core/release-checklist.html +3 -3
  113. nautobot/project-static/docs/development/core/role-internals.html +3 -3
  114. nautobot/project-static/docs/development/core/settings.html +3 -3
  115. nautobot/project-static/docs/development/core/style-guide.html +3 -3
  116. nautobot/project-static/docs/development/core/templates.html +3 -3
  117. nautobot/project-static/docs/development/core/testing.html +3 -3
  118. nautobot/project-static/docs/development/core/user-preferences.html +3 -3
  119. nautobot/project-static/docs/development/index.html +3 -3
  120. nautobot/project-static/docs/development/jobs/index.html +3 -3
  121. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -3
  122. nautobot/project-static/docs/index.html +3 -3
  123. nautobot/project-static/docs/objects.inv +0 -0
  124. nautobot/project-static/docs/overview/application_stack.html +3 -3
  125. nautobot/project-static/docs/overview/design_philosophy.html +3 -3
  126. nautobot/project-static/docs/release-notes/index.html +3 -3
  127. nautobot/project-static/docs/release-notes/version-1.0.html +3 -3
  128. nautobot/project-static/docs/release-notes/version-1.1.html +3 -3
  129. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  130. nautobot/project-static/docs/release-notes/version-1.3.html +3 -3
  131. nautobot/project-static/docs/release-notes/version-1.4.html +3 -3
  132. nautobot/project-static/docs/release-notes/version-1.5.html +3 -3
  133. nautobot/project-static/docs/release-notes/version-1.6.html +3 -3
  134. nautobot/project-static/docs/release-notes/version-2.0.html +3 -3
  135. nautobot/project-static/docs/release-notes/version-2.1.html +3 -3
  136. nautobot/project-static/docs/release-notes/version-2.2.html +3 -3
  137. nautobot/project-static/docs/release-notes/version-2.3.html +247 -96
  138. nautobot/project-static/docs/requirements.txt +1 -1
  139. nautobot/project-static/docs/search/search_index.json +1 -1
  140. nautobot/project-static/docs/sitemap.xml +269 -269
  141. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  142. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  143. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +3 -3
  144. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +3 -3
  145. nautobot/project-static/docs/user-guide/administration/configuration/index.html +3 -3
  146. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +3 -3
  147. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -3
  148. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +3 -3
  149. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
  150. nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -3
  151. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +3 -3
  152. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +3 -3
  153. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +3 -3
  154. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +3 -3
  155. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +3 -3
  156. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +3 -3
  157. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +3 -3
  158. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +3 -3
  159. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +3 -3
  160. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -3
  161. nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
  162. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +3 -3
  163. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +3 -3
  164. nautobot/project-static/docs/user-guide/administration/installation/services.html +3 -3
  165. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +3 -3
  166. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  167. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +3 -3
  168. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +3 -3
  169. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +3 -3
  170. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +3 -3
  171. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +3 -3
  172. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +3 -3
  173. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +3 -3
  175. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +3 -3
  176. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +3 -3
  177. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +3 -3
  178. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +3 -3
  179. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +3 -3
  180. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +3 -3
  181. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +3 -3
  182. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +3 -3
  183. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +3 -3
  184. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +3 -3
  185. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +3 -3
  186. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +3 -3
  187. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +3 -3
  188. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +3 -3
  189. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +3 -3
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +3 -3
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +3 -3
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +3 -3
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +3 -3
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +3 -3
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +3 -3
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +3 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +3 -3
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +3 -3
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +3 -3
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +3 -3
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +3 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +3 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +3 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +3 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +3 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +3 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +3 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +3 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +3 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +3 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +3 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +3 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +3 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +3 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +3 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +3 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +3 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +3 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +3 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +3 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +3 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +3 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +3 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +3 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +3 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +3 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +3 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +3 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +3 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +3 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +3 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +3 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +3 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +3 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +3 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +3 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +3 -3
  238. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +3 -3
  239. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +3 -3
  240. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +3 -3
  241. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +3 -3
  242. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +3 -3
  243. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
  244. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
  245. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
  246. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
  247. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
  248. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
  249. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
  250. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
  251. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
  252. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
  253. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  254. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +3 -3
  255. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
  256. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
  257. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +3 -3
  258. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
  259. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
  260. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
  261. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
  262. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +3 -3
  263. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
  264. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
  265. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
  266. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
  267. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
  268. nautobot/project-static/docs/user-guide/index.html +3 -3
  269. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
  270. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
  271. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
  272. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
  273. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
  274. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
  275. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
  276. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
  277. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
  278. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
  279. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
  280. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
  281. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +3 -3
  282. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
  283. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
  284. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +3 -3
  285. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +3 -3
  286. nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
  287. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +3 -3
  288. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +3 -3
  289. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +3 -3
  290. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +3 -3
  291. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +3 -3
  292. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +3 -3
  293. nautobot/project-static/docs/user-guide/platform-functionality/role.html +3 -3
  294. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +3 -3
  295. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
  296. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +3 -3
  297. nautobot/project-static/docs/user-guide/platform-functionality/status.html +3 -3
  298. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +3 -3
  299. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +3 -3
  300. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +3 -3
  301. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +3 -3
  302. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +3 -3
  303. nautobot/project-static/js/nav_menu.js +249 -0
  304. nautobot/tenancy/templates/tenancy/tenant.html +1 -1
  305. nautobot/users/tests/test_views.py +9 -11
  306. nautobot/virtualization/tests/test_views.py +3 -5
  307. {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/METADATA +2 -1
  308. {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/RECORD +312 -310
  309. {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/WHEEL +1 -1
  310. nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +0 -16
  311. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +0 -1
  312. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +0 -1
  313. {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/LICENSE.txt +0 -0
  314. {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/NOTICE +0 -0
  315. {nautobot-2.3.6.dist-info → nautobot-2.3.7.dist-info}/entry_points.txt +0 -0
@@ -864,11 +864,9 @@ class DynamicGroupTestCase(
864
864
  url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
865
865
  self.add_permissions("dcim.view_device", "extras.view_dynamicgroup")
866
866
  response = self.client.get(url)
867
- self.assertHttpStatus(response, 200)
868
- response_body = response.content.decode(response.charset)
869
- self.assertIn("DG 1", response_body, msg=response_body)
870
- self.assertIn("DG 2", response_body, msg=response_body)
871
- self.assertIn("DG 3", response_body, msg=response_body)
867
+ self.assertBodyContains(response, "DG 1")
868
+ self.assertBodyContains(response, "DG 2")
869
+ self.assertBodyContains(response, "DG 3")
872
870
 
873
871
  def test_get_object_dynamic_groups_with_constrained_permission(self):
874
872
  obj_perm = ObjectPermission(
@@ -891,7 +889,7 @@ class DynamicGroupTestCase(
891
889
  url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
892
890
  response = self.client.get(url)
893
891
  self.assertHttpStatus(response, 200)
894
- response_body = response.content.decode(response.charset)
892
+ response_body = extract_page_body(response.content.decode(response.charset))
895
893
  self.assertIn("DG 1", response_body, msg=response_body)
896
894
  self.assertNotIn("DG 2", response_body, msg=response_body)
897
895
  self.assertNotIn("DG 3", response_body, msg=response_body)
@@ -1334,18 +1332,14 @@ class SavedViewTest(ModelViewTestCase):
1334
1332
  # Try GET with model-level permission
1335
1333
  # SavedView detail view should redirect to the View from which it is derived
1336
1334
  response = self.client.get(instance.get_absolute_url(), follow=True)
1337
- self.assertHttpStatus(response, 200)
1338
- response_body = extract_page_body(response.content.decode(response.charset))
1339
- self.assertIn(escape(instance.name), response_body, msg=response_body)
1335
+ self.assertBodyContains(response, escape(instance.name))
1340
1336
 
1341
1337
  query_strings = ["&table_changes_pending=true", "&per_page=1234", "&status=active", "&sort=name"]
1342
1338
  for string in query_strings:
1343
1339
  view_url = self.get_view_url_for_saved_view(instance) + string
1344
1340
  response = self.client.get(view_url)
1345
- self.assertHttpStatus(response, 200)
1346
- response_body = extract_page_body(response.content.decode(response.charset))
1347
1341
  # Assert that the star sign is rendered on the page since there are unsaved changes
1348
- self.assertIn('<i title="Pending changes not saved">', response_body, msg=response_body)
1342
+ self.assertBodyContains(response, '<i title="Pending changes not saved">')
1349
1343
 
1350
1344
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1351
1345
  def test_get_object_with_constrained_permission(self):
@@ -1383,12 +1377,9 @@ class SavedViewTest(ModelViewTestCase):
1383
1377
  # Try update the saved view with a different user from the owner of the saved view
1384
1378
  self.client.force_login(different_user)
1385
1379
  response = self.client.get(update_url, follow=True)
1386
- self.assertHttpStatus(response, 200)
1387
- response_body = extract_page_body(response.content.decode(response.charset))
1388
- self.assertIn(
1380
+ self.assertBodyContains(
1381
+ response,
1389
1382
  f"You do not have the required permission to modify this Saved View owned by {instance.owner}",
1390
- response_body,
1391
- msg=response_body,
1392
1383
  )
1393
1384
 
1394
1385
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -1424,12 +1415,9 @@ class SavedViewTest(ModelViewTestCase):
1424
1415
  # Try delete the saved view with a different user from the owner of the saved view
1425
1416
  self.client.force_login(different_user)
1426
1417
  response = self.client.post(delete_url, follow=True)
1427
- self.assertHttpStatus(response, 200)
1428
- response_body = extract_page_body(response.content.decode(response.charset))
1429
- self.assertIn(
1418
+ self.assertBodyContains(
1419
+ response,
1430
1420
  f"You do not have the required permission to delete this Saved View owned by {instance.owner}",
1431
- response_body,
1432
- msg=response_body,
1433
1421
  )
1434
1422
 
1435
1423
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -1450,13 +1438,7 @@ class SavedViewTest(ModelViewTestCase):
1450
1438
  instance.owner.save()
1451
1439
  self.client.force_login(instance.owner)
1452
1440
  response = self.client.post(delete_url, follow=True)
1453
- self.assertHttpStatus(response, 200)
1454
- response_body = extract_page_body(response.content.decode(response.charset))
1455
- self.assertIn(
1456
- "Are you sure you want to delete saved view",
1457
- response_body,
1458
- msg=response_body,
1459
- )
1441
+ self.assertBodyContains(response, "Are you sure you want to delete saved view")
1460
1442
 
1461
1443
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1462
1444
  def test_create_saved_view(self):
@@ -1499,16 +1481,7 @@ class SavedViewTest(ModelViewTestCase):
1499
1481
  )
1500
1482
  response = self.client.get(reverse(view_name), follow=True)
1501
1483
  # Assert that Location List View got redirected to Saved View set as global default
1502
- self.assertHttpStatus(response, 200)
1503
- response_body = extract_page_body(response.content.decode(response.charset))
1504
- self.assertInHTML(
1505
- """
1506
- <strong>
1507
- Global Location Default View
1508
- </strong>
1509
- """,
1510
- response_body,
1511
- )
1484
+ self.assertBodyContains(response, "<strong>Global Location Default View</strong>", html=True)
1512
1485
 
1513
1486
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1514
1487
  def test_user_default(self):
@@ -1522,16 +1495,7 @@ class SavedViewTest(ModelViewTestCase):
1522
1495
  UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
1523
1496
  response = self.client.get(reverse(view_name), follow=True)
1524
1497
  # Assert that Location List View got redirected to Saved View set as user default
1525
- self.assertHttpStatus(response, 200)
1526
- response_body = extract_page_body(response.content.decode(response.charset))
1527
- self.assertInHTML(
1528
- """
1529
- <strong>
1530
- User Location Default View
1531
- </strong>
1532
- """,
1533
- response_body,
1534
- )
1498
+ self.assertBodyContains(response, "<strong>User Location Default View</strong>", html=True)
1535
1499
 
1536
1500
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1537
1501
  def test_user_default_precedes_global_default(self):
@@ -1550,16 +1514,7 @@ class SavedViewTest(ModelViewTestCase):
1550
1514
  UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
1551
1515
  response = self.client.get(reverse(view_name), follow=True)
1552
1516
  # Assert that Location List View got redirected to Saved View set as user default
1553
- self.assertHttpStatus(response, 200)
1554
- response_body = extract_page_body(response.content.decode(response.charset))
1555
- self.assertInHTML(
1556
- """
1557
- <strong>
1558
- User Location Default View
1559
- </strong>
1560
- """,
1561
- response_body,
1562
- )
1517
+ self.assertBodyContains(response, "<strong>User Location Default View</strong>", html=True)
1563
1518
 
1564
1519
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1565
1520
  def test_is_shared(self):
@@ -1914,12 +1869,8 @@ class ApprovalQueueTestCase(
1914
1869
 
1915
1870
  # Try GET with model-level permission
1916
1871
  response = self.client.get(self._get_url("view", instance))
1917
- self.assertHttpStatus(response, 200)
1918
-
1919
- response_body = extract_page_body(response.content.decode(response.charset))
1920
-
1921
1872
  # The object's display name or string representation should appear in the response
1922
- self.assertIn(getattr(instance, "display", str(instance)), response_body, msg=response_body)
1873
+ self.assertBodyContains(response, getattr(instance, "display", str(instance)))
1923
1874
 
1924
1875
  # skip GetObjectViewTestCase checks for Relationships and Custom Fields since this isn't actually a detail view
1925
1876
 
@@ -1954,9 +1905,7 @@ class ApprovalQueueTestCase(
1954
1905
  """Anonymous users may not take any action with regard to job approval requests."""
1955
1906
  self.client.logout()
1956
1907
  response = self.client.post(self._get_url("view", self._get_queryset().first()))
1957
- self.assertHttpStatus(response, 200)
1958
- response_body = extract_page_body(response.content.decode(response.charset))
1959
- self.assertIn("You do not have permission to run jobs", response_body)
1908
+ self.assertBodyContains(response, "You do not have permission to run jobs")
1960
1909
  # No job was submitted
1961
1910
  self.assertFalse(JobResult.objects.filter(name=self.job_model.name).exists())
1962
1911
 
@@ -1968,9 +1917,7 @@ class ApprovalQueueTestCase(
1968
1917
  data = {"_dry_run": True}
1969
1918
 
1970
1919
  response = self.client.post(self._get_url("view", instance), data)
1971
- self.assertHttpStatus(response, 200)
1972
- response_body = extract_page_body(response.content.decode(response.charset))
1973
- self.assertIn("This job cannot be run at this time", response_body)
1920
+ self.assertBodyContains(response, "This job cannot be run at this time")
1974
1921
  # No job was submitted
1975
1922
  self.assertFalse(JobResult.objects.filter(name=instance.job_model.name).exists())
1976
1923
 
@@ -1984,9 +1931,7 @@ class ApprovalQueueTestCase(
1984
1931
  data = {"_dry_run": True}
1985
1932
 
1986
1933
  response = self.client.post(self._get_url("view", instance), data)
1987
- self.assertHttpStatus(response, 200)
1988
- response_body = extract_page_body(response.content.decode(response.charset))
1989
- self.assertIn("You do not have permission to run this job", response_body)
1934
+ self.assertBodyContains(response, "You do not have permission to run this job")
1990
1935
  # No job was submitted
1991
1936
  self.assertFalse(JobResult.objects.filter(name=instance.job_model.name).exists())
1992
1937
 
@@ -2006,9 +1951,7 @@ class ApprovalQueueTestCase(
2006
1951
  instance2.job_model.save()
2007
1952
 
2008
1953
  response = self.client.post(self._get_url("view", instance2), data)
2009
- self.assertHttpStatus(response, 200)
2010
- response_body = extract_page_body(response.content.decode(response.charset))
2011
- self.assertIn("You do not have permission to run this job", response_body)
1954
+ self.assertBodyContains(response, "You do not have permission to run this job")
2012
1955
  # No job was submitted
2013
1956
  job_names = [instance1.job_model.name, instance2.job_model.name]
2014
1957
  self.assertFalse(JobResult.objects.filter(name__in=job_names).exists())
@@ -2085,9 +2028,7 @@ class ApprovalQueueTestCase(
2085
2028
  for user in (user1, user2):
2086
2029
  self.client.force_login(user)
2087
2030
  response = self.client.post(self._get_url("view", instance), data)
2088
- self.assertHttpStatus(response, 200, msg=str(user))
2089
- response_body = extract_page_body(response.content.decode(response.charset))
2090
- self.assertIn("You do not have permission", response_body, msg=str(user))
2031
+ self.assertBodyContains(response, "You do not have permission")
2091
2032
  # Request was not deleted
2092
2033
  self.assertEqual(1, len(ScheduledJob.objects.filter(pk=instance.pk)), msg=str(user))
2093
2034
 
@@ -2120,9 +2061,7 @@ class ApprovalQueueTestCase(
2120
2061
  # Check object-based permissions are enforced for a different instance
2121
2062
  instance = self._get_queryset().first()
2122
2063
  response = self.client.post(self._get_url("view", instance), data)
2123
- self.assertHttpStatus(response, 200, msg=str(user))
2124
- response_body = extract_page_body(response.content.decode(response.charset))
2125
- self.assertIn("You do not have permission", response_body, msg=str(user))
2064
+ self.assertBodyContains(response, "You do not have permission")
2126
2065
  # Request was not deleted
2127
2066
  self.assertEqual(1, len(ScheduledJob.objects.filter(pk=instance.pk)), msg=str(user))
2128
2067
 
@@ -2134,9 +2073,7 @@ class ApprovalQueueTestCase(
2134
2073
  data = {"_approve": True}
2135
2074
 
2136
2075
  response = self.client.post(self._get_url("view", instance), data)
2137
- self.assertHttpStatus(response, 200)
2138
- response_body = extract_page_body(response.content.decode(response.charset))
2139
- self.assertIn("You cannot approve your own job request", response_body)
2076
+ self.assertBodyContains(response, "You cannot approve your own job request")
2140
2077
  # Job was not approved
2141
2078
  instance.refresh_from_db()
2142
2079
  self.assertIsNone(instance.approved_by_user)
@@ -2171,9 +2108,7 @@ class ApprovalQueueTestCase(
2171
2108
  for user in (user1, user2):
2172
2109
  self.client.force_login(user)
2173
2110
  response = self.client.post(self._get_url("view", instance), data)
2174
- self.assertHttpStatus(response, 200, msg=str(user))
2175
- response_body = extract_page_body(response.content.decode(response.charset))
2176
- self.assertIn("You do not have permission", response_body, msg=str(user))
2111
+ self.assertBodyContains(response, "You do not have permission")
2177
2112
  # Job was not approved
2178
2113
  instance.refresh_from_db()
2179
2114
  self.assertIsNone(instance.approved_by_user)
@@ -2208,9 +2143,7 @@ class ApprovalQueueTestCase(
2208
2143
  # Check object-based permissions are enforced for a different instance
2209
2144
  instance = self._get_queryset().last()
2210
2145
  response = self.client.post(self._get_url("view", instance), data)
2211
- self.assertHttpStatus(response, 200, msg=str(user))
2212
- response_body = extract_page_body(response.content.decode(response.charset))
2213
- self.assertIn("You do not have permission", response_body, msg=str(user))
2146
+ self.assertBodyContains(response, "You do not have permission")
2214
2147
  # Job was not scheduled
2215
2148
  instance.refresh_from_db()
2216
2149
  self.assertIsNone(instance.approved_by_user)
@@ -2251,9 +2184,7 @@ class JobResultTestCase(
2251
2184
  url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
2252
2185
  self.add_permissions("extras.view_jobresult", "extras.view_joblogentry")
2253
2186
  response = self.client.get(url)
2254
- self.assertHttpStatus(response, 200)
2255
- response_body = response.content.decode(response.charset)
2256
- self.assertIn("This is a test", response_body)
2187
+ self.assertBodyContains(response, "This is a test")
2257
2188
 
2258
2189
  # TODO test with constrained permissions on both JobResult and JobLogEntry records
2259
2190
 
@@ -2394,18 +2325,18 @@ class JobTestCase(
2394
2325
  # Try delete with delete job permission
2395
2326
  self.add_permissions("extras.delete_job")
2396
2327
  response = self.client.post(**request, follow=True)
2397
- self.assertHttpStatus(response, 403)
2398
- response_body = extract_page_body(response.content.decode(response.charset))
2399
- self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
2328
+ self.assertBodyContains(
2329
+ response, f"Unable to delete Job {instance}. System Job cannot be deleted", status_code=403
2330
+ )
2400
2331
  # assert Job still exists
2401
2332
  self.assertTrue(self._get_queryset().filter(name=job_name).exists())
2402
2333
 
2403
2334
  # Try delete as a superuser
2404
2335
  self.user.is_superuser = True
2405
2336
  response = self.client.post(**request, follow=True)
2406
- self.assertHttpStatus(response, 403)
2407
- response_body = extract_page_body(response.content.decode(response.charset))
2408
- self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
2337
+ self.assertBodyContains(
2338
+ response, f"Unable to delete Job {instance}. System Job cannot be deleted", status_code=403
2339
+ )
2409
2340
  # assert Job still exists
2410
2341
  self.assertTrue(self._get_queryset().filter(name=job_name).exists())
2411
2342
 
@@ -2421,22 +2352,22 @@ class JobTestCase(
2421
2352
  # Try bulk delete with delete job permission
2422
2353
  self.add_permissions("extras.delete_job")
2423
2354
  response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2424
- self.assertHttpStatus(response, 403)
2425
- self.assertEqual(self._get_queryset().count(), initial_count)
2426
- response_body = extract_page_body(response.content.decode(response.charset))
2427
- self.assertIn(
2428
- f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
2355
+ self.assertBodyContains(
2356
+ response,
2357
+ f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted",
2358
+ status_code=403,
2429
2359
  )
2360
+ self.assertEqual(self._get_queryset().count(), initial_count)
2430
2361
 
2431
2362
  # Try bulk delete as a superuser
2432
2363
  self.user.is_superuser = True
2433
2364
  response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
2434
- self.assertHttpStatus(response, 403)
2435
- self.assertEqual(self._get_queryset().count(), initial_count)
2436
- response_body = extract_page_body(response.content.decode(response.charset))
2437
- self.assertIn(
2438
- f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
2365
+ self.assertBodyContains(
2366
+ response,
2367
+ f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted",
2368
+ status_code=403,
2439
2369
  )
2370
+ self.assertEqual(self._get_queryset().count(), initial_count)
2440
2371
 
2441
2372
  def validate_job_data_after_bulk_edit(self, pk_list, old_data):
2442
2373
  # Name is bulk-editable
@@ -2502,10 +2433,7 @@ class JobTestCase(
2502
2433
  self.add_permissions("extras.run_job")
2503
2434
  for run_url in self.run_urls:
2504
2435
  response = self.client.get(run_url)
2505
- self.assertHttpStatus(response, 200, msg=run_url)
2506
-
2507
- response_body = extract_page_body(response.content.decode(response.charset))
2508
- self.assertIn("TestPass", response_body)
2436
+ self.assertBodyContains(response, "TestPass")
2509
2437
 
2510
2438
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
2511
2439
  def test_get_run_with_constrained_permission(self):
@@ -2548,10 +2476,7 @@ class JobTestCase(
2548
2476
 
2549
2477
  for run_url in self.run_urls:
2550
2478
  response = self.client.post(run_url, self.data_run_immediately)
2551
- self.assertHttpStatus(response, 200, msg=run_url)
2552
-
2553
- content = extract_page_body(response.content.decode(response.charset))
2554
- self.assertIn("Celery worker process not running.", content)
2479
+ self.assertBodyContains(response, "Celery worker process not running.")
2555
2480
 
2556
2481
  @mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
2557
2482
  def test_run_now(self, _):
@@ -2598,9 +2523,7 @@ class JobTestCase(
2598
2523
  reverse("extras:job_run", kwargs={"pk": self.test_not_installed.pk}),
2599
2524
  ):
2600
2525
  response = self.client.post(run_url, self.data_run_immediately)
2601
- self.assertEqual(response.status_code, 200, msg=run_url)
2602
- response_body = extract_page_body(response.content.decode(response.charset))
2603
- self.assertIn("Job is not presently installed", response_body)
2526
+ self.assertBodyContains(response, "Job is not presently installed")
2604
2527
 
2605
2528
  self.assertFalse(JobResult.objects.filter(name=self.test_not_installed.name).exists())
2606
2529
 
@@ -2613,9 +2536,7 @@ class JobTestCase(
2613
2536
  reverse("extras:job_run", kwargs={"pk": Job.objects.get(job_class_name="TestFail").pk}),
2614
2537
  ):
2615
2538
  response = self.client.post(run_url, self.data_run_immediately)
2616
- self.assertEqual(response.status_code, 200, msg=run_url)
2617
- response_body = extract_page_body(response.content.decode(response.charset))
2618
- self.assertIn("Job is not enabled to be run", response_body)
2539
+ self.assertBodyContains(response, "Job is not enabled to be run")
2619
2540
  self.assertFalse(JobResult.objects.filter(name="fail.TestFail").exists())
2620
2541
 
2621
2542
  def test_run_now_missing_args(self):
@@ -2773,10 +2694,7 @@ class JobTestCase(
2773
2694
  for i, run_url in enumerate(self.run_urls):
2774
2695
  data["_schedule_name"] = f"test {i}"
2775
2696
  response = self.client.post(run_url, data)
2776
- self.assertHttpStatus(response, 200, msg=self.run_urls[1])
2777
-
2778
- content = extract_page_body(response.content.decode(response.charset))
2779
- self.assertIn("Unable to schedule job: Job may have sensitive input variables.", content)
2697
+ self.assertBodyContains(response, "Unable to schedule job: Job may have sensitive input variables.")
2780
2698
 
2781
2699
  @mock.patch("nautobot.extras.views.get_worker_count", return_value=1)
2782
2700
  def test_run_job_with_invalid_task_queue(self, _):
@@ -2817,31 +2735,28 @@ class JobTestCase(
2817
2735
  for run_url in self.run_urls:
2818
2736
  # Assert warning message shows in get
2819
2737
  response = self.client.get(run_url)
2820
- content = extract_page_body(response.content.decode(response.charset))
2821
- self.assertIn(
2738
+ self.assertBodyContains(
2739
+ response,
2822
2740
  "This job is flagged as possibly having sensitive variables but is also flagged as requiring approval.",
2823
- content,
2824
2741
  )
2825
2742
 
2826
2743
  # Assert run button is disabled
2827
- self.assertInHTML(
2744
+ self.assertBodyContains(
2745
+ response,
2828
2746
  """
2829
2747
  <button type="submit" name="_run" id="id__run" class="btn btn-primary" disabled="disabled">
2830
2748
  <i class="mdi mdi-play"></i> Run Job Now
2831
2749
  </button>
2832
2750
  """,
2833
- content,
2751
+ html=True,
2834
2752
  )
2835
2753
  # Assert error message shows after post
2836
2754
  response = self.client.post(run_url, data)
2837
- self.assertHttpStatus(response, 200, msg=self.run_urls[1])
2838
-
2839
- content = extract_page_body(response.content.decode(response.charset))
2840
- self.assertIn(
2755
+ self.assertBodyContains(
2756
+ response,
2841
2757
  "Unable to run or schedule job: "
2842
2758
  "This job is flagged as possibly having sensitive variables but is also flagged as requiring approval."
2843
2759
  "One of these two flags must be removed before this job can be scheduled or run.",
2844
- content,
2845
2760
  )
2846
2761
 
2847
2762
  def test_job_object_change_log_view(self):
@@ -2849,10 +2764,7 @@ class JobTestCase(
2849
2764
  instance = self.test_pass
2850
2765
  self.add_permissions("extras.view_objectchange", "extras.view_job")
2851
2766
  response = self.client.get(instance.get_changelog_url())
2852
- content = extract_page_body(response.content.decode(response.charset))
2853
-
2854
- self.assertHttpStatus(response, 200)
2855
- self.assertIn(f"{instance.name} - Change Log", content)
2767
+ self.assertBodyContains(response, f"{instance.name} - Change Log")
2856
2768
 
2857
2769
 
2858
2770
  class JobButtonTestCase(
@@ -2949,10 +2861,8 @@ class JobButtonRenderingTestCase(TestCase):
2949
2861
  def test_view_object_with_job_button(self):
2950
2862
  """Ensure that the job button is rendered."""
2951
2863
  response = self.client.get(self.location_type.get_absolute_url(), follow=True)
2952
- self.assertEqual(response.status_code, 200)
2953
- content = extract_page_body(response.content.decode(response.charset))
2954
- self.assertIn(f"JobButton {self.location_type.name}", content, content)
2955
- self.assertIn("Click me!", content, content)
2864
+ self.assertBodyContains(response, f"JobButton {self.location_type.name}")
2865
+ self.assertBodyContains(response, "Click me!")
2956
2866
 
2957
2867
  def test_task_queue_hidden_input_is_present(self):
2958
2868
  """
@@ -2962,16 +2872,12 @@ class JobButtonRenderingTestCase(TestCase):
2962
2872
  self.job.task_queues = ["overriden_queue", "default", "priority"]
2963
2873
  self.job.save()
2964
2874
  response = self.client.get(self.location_type.get_absolute_url(), follow=True)
2965
- self.assertEqual(response.status_code, 200)
2966
- content = extract_page_body(response.content.decode(response.charset))
2967
- self.assertIn(f'<input type="hidden" name="_task_queue" value="{self.job.task_queues[0]}">', content, content)
2875
+ self.assertBodyContains(response, f'<input type="hidden" name="_task_queue" value="{self.job.task_queues[0]}">')
2968
2876
  self.job.task_queues_override = False
2969
2877
  self.job.save()
2970
2878
  response = self.client.get(self.location_type.get_absolute_url(), follow=True)
2971
- self.assertEqual(response.status_code, 200)
2972
- content = extract_page_body(response.content.decode(response.charset))
2973
- self.assertIn(
2974
- f'<input type="hidden" name="_task_queue" value="{settings.CELERY_TASK_DEFAULT_QUEUE}">', content, content
2879
+ self.assertBodyContains(
2880
+ response, f'<input type="hidden" name="_task_queue" value="{settings.CELERY_TASK_DEFAULT_QUEUE}">'
2975
2881
  )
2976
2882
 
2977
2883
  def test_view_object_with_unsafe_text(self):
@@ -3426,7 +3332,6 @@ class RelationshipAssociationTestCase(
3426
3332
  response = self.client.get(self._get_url("list"))
3427
3333
  self.assertHttpStatus(response, 200)
3428
3334
  content = extract_page_body(response.content.decode(response.charset))
3429
- # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
3430
3335
  self.assertIn(instance1.source.name, content, msg=content)
3431
3336
  self.assertIn(instance1.destination.name, content, msg=content)
3432
3337
  self.assertNotIn(instance2.source.name, content, msg=content)
@@ -3470,10 +3375,7 @@ class StaticGroupAssociationTestCase(
3470
3375
 
3471
3376
  self.add_permissions("extras.view_staticgroupassociation")
3472
3377
  response = self.client.get(f"{self._get_url('list')}?dynamic_group={sga1.dynamic_group.pk}")
3473
- self.assertHttpStatus(response, 200)
3474
- content = extract_page_body(response.content.decode(response.charset))
3475
-
3476
- self.assertIn(sga1.get_absolute_url(), content, msg=content)
3378
+ self.assertBodyContains(response, sga1.get_absolute_url())
3477
3379
 
3478
3380
 
3479
3381
  class StatusTestCase(
@@ -3610,7 +3512,7 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
3610
3512
  response = self.client.post(**request)
3611
3513
  tag = Tag.objects.filter(name=self.form_data["name"])
3612
3514
  self.assertFalse(tag.exists())
3613
- self.assertIn("content_types: Select a valid choice", str(response.content))
3515
+ self.assertBodyContains(response, "content_types: Select a valid choice")
3614
3516
 
3615
3517
  def test_update_tags_remove_content_type(self):
3616
3518
  """Test removing a tag content_type that is been tagged to a model"""
nautobot/extras/utils.py CHANGED
@@ -21,7 +21,7 @@ from nautobot.core.choices import ColorChoices
21
21
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
22
22
  from nautobot.core.models.managers import TagsManager
23
23
  from nautobot.core.models.utils import find_models_with_matching_fields
24
- from nautobot.extras.choices import ObjectChangeActionChoices
24
+ from nautobot.extras.choices import DynamicGroupTypeChoices, ObjectChangeActionChoices
25
25
  from nautobot.extras.constants import (
26
26
  CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL,
27
27
  EXTRAS_FEATURES,
@@ -366,17 +366,30 @@ def get_celery_queues():
366
366
  """
367
367
  from nautobot.core.celery import app # prevent circular import
368
368
 
369
- celery_queues = {}
369
+ celery_queues = None
370
+ with contextlib.suppress(redis.exceptions.ConnectionError):
371
+ celery_queues = cache.get("nautobot.extras.utils.get_celery_queues")
370
372
 
371
- celery_inspect = app.control.inspect()
372
- active_queues = celery_inspect.active_queues()
373
- if active_queues is None:
374
- return celery_queues
375
- for task_queue_list in active_queues.values():
376
- distinct_queues = {q["name"] for q in task_queue_list}
377
- for queue in distinct_queues:
378
- celery_queues.setdefault(queue, 0)
379
- celery_queues[queue] += 1
373
+ if celery_queues is None:
374
+ celery_queues = {}
375
+ celery_inspect = app.control.inspect()
376
+ try:
377
+ active_queues = celery_inspect.active_queues()
378
+ except redis.exceptions.ConnectionError:
379
+ # Celery seems to be not smart enough to auto-retry on intermittent failures, so let's do it ourselves:
380
+ try:
381
+ active_queues = celery_inspect.active_queues()
382
+ except redis.exceptions.ConnectionError as err:
383
+ logger.error("Repeated ConnectionError from Celery/Redis: %s", err)
384
+ active_queues = None
385
+ if active_queues is None:
386
+ return celery_queues
387
+ for task_queue_list in active_queues.values():
388
+ distinct_queues = {q["name"] for q in task_queue_list}
389
+ for queue in distinct_queues:
390
+ celery_queues[queue] = celery_queues.get(queue, 0) + 1
391
+ with contextlib.suppress(redis.exceptions.ConnectionError):
392
+ cache.set("nautobot.extras.utils.get_celery_queues", celery_queues, timeout=5)
380
393
 
381
394
  return celery_queues
382
395
 
@@ -584,6 +597,32 @@ def fixup_null_statuses(*, model, model_contenttype, status_model):
584
597
  print(f" Found and fixed {updated_count} instances of {model.__name__} that had null 'status' fields.")
585
598
 
586
599
 
600
+ def fixup_dynamic_group_group_types(apps, *args, **kwargs): # pylint: disable=redefined-outer-name
601
+ """Set dynamic group group_type values correctly."""
602
+ DynamicGroup = apps.get_model("extras", "DynamicGroup")
603
+ DynamicGroupMembership = apps.get_model("extras", "DynamicGroupMembership")
604
+ count_1 = count_2 = 0
605
+ # See note in migration 0112 - for some reason, if we were to do the "intuitive" thing, and call
606
+ # `DynamicGroup.objects.filter(children__isnull=False)`, we would unexpectedly get those groups for which their
607
+ # *parent* is non-null. The below is an alternate approach that should remain correct even if that issue gets fixed.
608
+ parent_group_names = set(DynamicGroupMembership.objects.values_list("parent_group__name", flat=True))
609
+ parent_groups_with_wrong_type = DynamicGroup.objects.filter(name__in=parent_group_names).exclude(
610
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
611
+ )
612
+ if parent_groups_with_wrong_type.exists():
613
+ count_1 = parent_groups_with_wrong_type.update(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET)
614
+ print(f'\n Found and fixed {count_1} DynamicGroup(s) that should be typed as "Group of groups".')
615
+
616
+ filter_groups_with_wrong_type = DynamicGroup.objects.exclude(filter__exact={}).exclude(
617
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
618
+ )
619
+ if filter_groups_with_wrong_type.exists():
620
+ count_2 = filter_groups_with_wrong_type.update(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER)
621
+ print(f'\n Found and fixed {count_2} DynamicGroup(s) that should be typed as "Filter-defined".')
622
+
623
+ return count_1, count_2
624
+
625
+
587
626
  def migrate_role_data(
588
627
  model_to_migrate,
589
628
  *,
@@ -200,14 +200,19 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
200
200
  }
201
201
  self.add_permissions("ipam.add_vrfdeviceassignment")
202
202
  response = self.client.post(self._get_list_url(), duplicate_device_create_data, format="json", **self.header)
203
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
204
- self.assertIn("The fields device, vrf must make a unique set.", str(response.content))
203
+ self.assertContains(
204
+ response, "The fields device, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
205
+ )
205
206
  response = self.client.post(self._get_list_url(), duplicate_vm_create_data, format="json", **self.header)
206
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
207
- self.assertIn("The fields virtual_machine, vrf must make a unique set.", str(response.content))
207
+ self.assertContains(
208
+ response, "The fields virtual_machine, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
209
+ )
208
210
  response = self.client.post(self._get_list_url(), invalid_create_data, format="json", **self.header)
209
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
210
- self.assertIn("A VRF cannot be associated with both a device and a virtual machine.", str(response.content))
211
+ self.assertContains(
212
+ response,
213
+ "A VRF cannot be associated with both a device and a virtual machine.",
214
+ status_code=status.HTTP_400_BAD_REQUEST,
215
+ )
211
216
 
212
217
 
213
218
  class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
@@ -266,14 +271,15 @@ class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
266
271
  }
267
272
  self.add_permissions("ipam.add_vrfprefixassignment")
268
273
  response = self.client.post(self._get_list_url(), duplicate_create_data, format="json", **self.header)
269
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
270
- self.assertIn("The fields vrf, prefix must make a unique set.", str(response.content))
274
+ self.assertContains(
275
+ response, "The fields vrf, prefix must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
276
+ )
271
277
  response = self.client.post(self._get_list_url(), wrong_namespace_create_data, format="json", **self.header)
272
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
273
- self.assertIn("Prefix must be in same namespace as VRF", str(response.content))
278
+ self.assertContains(
279
+ response, "Prefix must be in same namespace as VRF", status_code=status.HTTP_400_BAD_REQUEST
280
+ )
274
281
  response = self.client.post(self._get_list_url(), missing_field_create_data, format="json", **self.header)
275
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
276
- self.assertIn("This field may not be null.", str(response.content))
282
+ self.assertContains(response, "This field may not be null.", status_code=status.HTTP_400_BAD_REQUEST)
277
283
 
278
284
 
279
285
  class RouteTargetTest(APIViewTestCases.APIViewTestCase):