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
@@ -144,6 +144,7 @@ class ViewTestCases:
144
144
  self.client.logout()
145
145
  response = self.client.get(self._get_queryset().first().get_absolute_url())
146
146
  self.assertHttpStatus(response, 200)
147
+ # TODO: all this is doing is checking that a login link appears somewhere on the page (i.e. in the nav).
147
148
  response_body = response.content.decode(response.charset)
148
149
  self.assertIn(
149
150
  "/login/?next=" + self._get_queryset().first().get_absolute_url(), response_body, msg=response_body
@@ -151,8 +152,7 @@ class ViewTestCases:
151
152
 
152
153
  # The "Change Log" tab should appear in the response since we have all exempt permissions
153
154
  if issubclass(self.model, extras_models.ChangeLoggedModel):
154
- response_body = utils.extract_page_body(response.content.decode(response.charset))
155
- self.assertIn("Change Log", response_body, msg=response_body)
155
+ self.assertBodyContains(response, "Change Log")
156
156
 
157
157
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
158
158
  def test_get_object_without_permission(self):
@@ -177,41 +177,33 @@ class ViewTestCases:
177
177
 
178
178
  # Try GET with model-level permission
179
179
  response = self.client.get(instance.get_absolute_url())
180
- self.assertHttpStatus(response, 200)
181
-
182
- response_body = utils.extract_page_body(response.content.decode(response.charset))
183
-
184
- # The object's display name or string representation should appear in the response
185
- self.assertIn(escape(getattr(instance, "display", str(instance))), response_body, msg=response_body)
180
+ # The object's display name or string representation should appear in the response body
181
+ self.assertBodyContains(response, escape(getattr(instance, "display", str(instance))))
186
182
 
187
183
  # If any Relationships are defined, they should appear in the response
188
184
  if self.relationships is not None:
189
185
  for relationship in self.relationships: # false positive pylint: disable=not-an-iterable
190
186
  content_type = ContentType.objects.get_for_model(instance)
191
187
  if content_type == relationship.source_type:
192
- self.assertIn(
188
+ self.assertBodyContains(
189
+ response,
193
190
  escape(relationship.get_label(extras_choices.RelationshipSideChoices.SIDE_SOURCE)),
194
- response_body,
195
- msg=response_body,
196
191
  )
197
192
  if content_type == relationship.destination_type:
198
- self.assertIn(
193
+ self.assertBodyContains(
194
+ response,
199
195
  escape(relationship.get_label(extras_choices.RelationshipSideChoices.SIDE_DESTINATION)),
200
- response_body,
201
- msg=response_body,
202
196
  )
203
197
 
204
198
  # If any Custom Fields are defined, they should appear in the response
205
199
  if self.custom_fields is not None:
206
200
  for custom_field in self.custom_fields: # false positive pylint: disable=not-an-iterable
207
- self.assertIn(escape(str(custom_field)), response_body, msg=response_body)
201
+ self.assertBodyContains(response, escape(str(custom_field)))
208
202
  if custom_field.type == extras_choices.CustomFieldTypeChoices.TYPE_MULTISELECT:
209
203
  for value in instance.cf.get(custom_field.key):
210
- self.assertIn(escape(str(value)), response_body, msg=response_body)
204
+ self.assertBodyContains(response, escape(str(value)))
211
205
  else:
212
- self.assertIn(
213
- escape(str(instance.cf.get(custom_field.key) or "")), response_body, msg=response_body
214
- )
206
+ self.assertBodyContains(response, escape(str(instance.cf.get(custom_field.key) or "")))
215
207
 
216
208
  return response # for consumption by child test cases if desired
217
209
 
@@ -251,11 +243,8 @@ class ViewTestCases:
251
243
  obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
252
244
 
253
245
  response = self.client.get(instance.get_absolute_url())
254
- response_body = utils.extract_page_body(response.content.decode(response.charset))
255
- advanced_tab_href = f"{instance.get_absolute_url()}#advanced"
256
-
257
- self.assertIn(advanced_tab_href, response_body)
258
- self.assertIn("Advanced", response_body)
246
+ self.assertBodyContains(response, f"{instance.get_absolute_url()}#advanced")
247
+ self.assertBodyContains(response, "Advanced")
259
248
 
260
249
  class GetObjectChangelogViewTestCase(ModelViewTestCase):
261
250
  """
@@ -268,12 +257,16 @@ class ViewTestCases:
268
257
  url = self._get_url("changelog", obj)
269
258
  response = self.client.get(url)
270
259
  self.assertHttpStatus(response, 200)
271
- response_data = response.content.decode(response.charset)
260
+
261
+ # Test for https://github.com/nautobot/nautobot/issues/5214
272
262
  if getattr(obj, "is_contact_associable_model", False):
273
- self.assertInHTML(
263
+ self.assertBodyContains(
264
+ response,
274
265
  f'<a href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" aria-controls="contacts" role="tab" data-toggle="tab">Contacts</a>',
275
- response_data,
266
+ html=True,
276
267
  )
268
+ else:
269
+ self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
277
270
 
278
271
  class GetObjectNotesViewTestCase(ModelViewTestCase):
279
272
  """
@@ -287,12 +280,16 @@ class ViewTestCases:
287
280
  url = self._get_url("notes", obj)
288
281
  response = self.client.get(url)
289
282
  self.assertHttpStatus(response, 200)
290
- response_data = response.content.decode(response.charset)
283
+
284
+ # Test for https://github.com/nautobot/nautobot/issues/5214
291
285
  if getattr(obj, "is_contact_associable_model", False):
292
- self.assertInHTML(
286
+ self.assertBodyContains(
287
+ response,
293
288
  f'<a href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" aria-controls="contacts" role="tab" data-toggle="tab">Contacts</a>',
294
- response_data,
289
+ html=True,
295
290
  )
291
+ else:
292
+ self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
296
293
 
297
294
  class CreateObjectViewTestCase(ModelViewTestCase):
298
295
  """
@@ -366,11 +363,9 @@ class ViewTestCases:
366
363
  detail_url = instance.get_absolute_url()
367
364
  validate(detail_url)
368
365
  response = self.client.get(detail_url)
369
- response_body = utils.extract_page_body(response.content.decode(response.charset))
370
- advanced_tab_href = f"{detail_url}#advanced"
371
- self.assertIn(advanced_tab_href, response_body)
372
- self.assertIn("<td>Created By</td>", response_body)
373
- self.assertIn("<td>nautobotuser</td>", response_body)
366
+ self.assertBodyContains(response, f"{detail_url}#advanced")
367
+ self.assertBodyContains(response, "<td>Created By</td>", html=True)
368
+ self.assertBodyContains(response, f"<td>{self.user.username}</td>", html=True)
374
369
  except (AttributeError, ValidationError):
375
370
  # Instance does not have a valid detail view, do nothing here.
376
371
  pass
@@ -522,11 +517,9 @@ class ViewTestCases:
522
517
  detail_url = instance.get_absolute_url()
523
518
  validate(detail_url)
524
519
  response = self.client.get(detail_url)
525
- response_body = utils.extract_page_body(response.content.decode(response.charset))
526
- advanced_tab_href = f"{detail_url}#advanced"
527
- self.assertIn(advanced_tab_href, response_body)
528
- self.assertIn("<td>Last Updated By</td>", response_body)
529
- self.assertIn("<td>nautobotuser</td>", response_body)
520
+ self.assertBodyContains(response, f"{detail_url}#advanced")
521
+ self.assertBodyContains(response, "<td>Last Updated By</td>", html=True)
522
+ self.assertBodyContains(response, f"<td>{self.user.username}</td>", html=True)
530
523
  except (AttributeError, ValidationError):
531
524
  # Instance does not have a valid detail view, do nothing here.
532
525
  pass
@@ -763,8 +756,7 @@ class ViewTestCases:
763
756
 
764
757
  with self.subTest("Assert indentation is present"):
765
758
  response = self.client.get(f"{self._get_url('list')}")
766
- response_body = response.content.decode(response.charset)
767
- self.assertInHTML('<i class="mdi mdi-circle-small"></i>', response_body)
759
+ self.assertBodyContains(response, '<i class="mdi mdi-circle-small"></i>', html=True)
768
760
 
769
761
  with self.subTest("Assert indentation is removed on filter"):
770
762
  queryset = (
@@ -786,6 +778,7 @@ class ViewTestCases:
786
778
  self.client.logout()
787
779
  response = self.client.get(self._get_url("list"))
788
780
  self.assertHttpStatus(response, 200)
781
+ # TODO: all this is doing is checking that a login link appears somewhere on the page (i.e. in the nav).
789
782
  response_body = response.content.decode(response.charset)
790
783
  self.assertIn("/login/?next=" + self._get_url("list"), response_body, msg=response_body)
791
784
 
@@ -799,7 +792,6 @@ class ViewTestCases:
799
792
  response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
800
793
  self.assertHttpStatus(response, 200)
801
794
  content = utils.extract_page_body(response.content.decode(response.charset))
802
- # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
803
795
  if hasattr(self.model, "name"):
804
796
  self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
805
797
  self.assertNotRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
@@ -810,12 +802,9 @@ class ViewTestCases:
810
802
  def test_list_objects_unknown_filter_strict_filtering(self):
811
803
  """Verify that with STRICT_FILTERING, an unknown filter results in an error message and no matches."""
812
804
  response = self.client.get(f"{self._get_url('list')}?ice_cream_flavor=chocolate")
813
- self.assertHttpStatus(response, 200)
814
- content = utils.extract_page_body(response.content.decode(response.charset))
815
- # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
816
- self.assertIn("Unknown filter field", content, msg=content)
805
+ self.assertBodyContains(response, "Unknown filter field")
817
806
  # There should be no table rows displayed except for the empty results row
818
- self.assertIn("None", content, msg=content)
807
+ self.assertBodyContains(response, "None")
819
808
 
820
809
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"], STRICT_FILTERING=False)
821
810
  def test_list_objects_unknown_filter_no_strict_filtering(self):
@@ -842,7 +831,6 @@ class ViewTestCases:
842
831
  )
843
832
  self.assertHttpStatus(response, 200)
844
833
  content = utils.extract_page_body(response.content.decode(response.charset))
845
- # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
846
834
  self.assertNotIn("Unknown filter field", content, msg=content)
847
835
  self.assertIn("None", content, msg=content)
848
836
  if hasattr(self.model, "name"):
@@ -871,16 +859,13 @@ class ViewTestCases:
871
859
  # Try GET with model-level permission
872
860
  response = self.client.get(self._get_url("list"))
873
861
  self.assertHttpStatus(response, 200)
874
- response_body = response.content.decode(response.charset)
862
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
875
863
 
876
864
  list_url = self.get_list_url()
877
865
  title = self.get_title()
878
866
 
879
867
  # Check if breadcrumb is rendered correctly
880
- self.assertIn(
881
- f'<a href="{list_url}">{title}</a>',
882
- response_body,
883
- )
868
+ self.assertBodyContains(response, f'<a href="{list_url}">{title}</a>', html=True)
884
869
 
885
870
  # Check if import button is absent due to user permissions
886
871
  self.assertNotIn(
@@ -909,7 +894,6 @@ class ViewTestCases:
909
894
  response = self.client.get(self._get_url("list"))
910
895
  self.assertHttpStatus(response, 200)
911
896
  content = utils.extract_page_body(response.content.decode(response.charset))
912
- # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
913
897
  if hasattr(self.model, "name"):
914
898
  self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
915
899
  self.assertNotRegex(content, r">\s*" + re.escape(escape(instance2.name)) + r"\s*<", msg=content)
@@ -953,12 +937,12 @@ class ViewTestCases:
953
937
 
954
938
  # Try GET with model-level permission
955
939
  response = self.client.get(self._get_url("list"))
956
- self.assertHttpStatus(response, 200)
957
- response_body = response.content.decode(response.charset)
958
940
 
959
941
  # Check app banner is rendered correctly
960
- self.assertIn(
961
- f"<div>You are viewing a table of {self.model._meta.verbose_name_plural}</div>", response_body
942
+ self.assertBodyContains(
943
+ response,
944
+ f"<div>You are viewing a table of {self.model._meta.verbose_name_plural}</div>",
945
+ html=True,
962
946
  )
963
947
 
964
948
  class CreateMultipleObjectsViewTestCase(ModelViewTestCase):
@@ -1596,7 +1580,7 @@ class ViewTestCases:
1596
1580
  f"Renaming {len(objects)} {helpers.bettertitle(verbose_name_plural)} "
1597
1581
  f"on {self.selected_objects_parent_name}"
1598
1582
  )
1599
- self.assertInHTML(message, response.content.decode(response.charset))
1583
+ self.assertBodyContains(response, message)
1600
1584
 
1601
1585
  with self.subTest("Assert update successfully"):
1602
1586
  data["_apply"] = True # Form Apply button
@@ -1611,4 +1595,4 @@ class ViewTestCases:
1611
1595
  data["pk"] = values
1612
1596
  response = self.client.post(self._get_url("bulk_rename"), data, follow=True)
1613
1597
  expected_message = f"No valid {verbose_name_plural} were selected."
1614
- self.assertIn(expected_message, response.content.decode(response.charset))
1598
+ self.assertBodyContains(response, expected_message)
@@ -157,8 +157,9 @@ class NautobotTestRunner(DiscoverRunner):
157
157
  suffix=str(index + 1),
158
158
  verbosity=self.verbosity,
159
159
  keepdb=self.keepdb
160
- # Extra check added for Nautobot:
161
- and self.reusedb,
160
+ # Extra checks added for Nautobot:
161
+ and self.reusedb
162
+ and not settings.TEST_USE_FACTORIES, # w/ factory data, clones can't be reused
162
163
  )
163
164
 
164
165
  # Configure all other connections as mirrors of the first one
@@ -188,7 +189,9 @@ class NautobotTestRunner(DiscoverRunner):
188
189
  connection.creation.destroy_test_db(
189
190
  suffix=str(index + 1),
190
191
  verbosity=self.verbosity,
191
- keepdb=self.keepdb,
192
+ keepdb=self.keepdb
193
+ # Extra check added for Nautobot
194
+ and not settings.TEST_USE_FACTORIES, # with factory data, clones cannot be reused
192
195
  )
193
196
 
194
197
  # Extra block added for Nautobot
@@ -8,6 +8,7 @@ from django.urls import reverse
8
8
 
9
9
  from nautobot.circuits import models as circuits_models
10
10
  from nautobot.core import testing
11
+ from nautobot.core.testing.utils import extract_page_body
11
12
  from nautobot.core.views import paginator
12
13
  from nautobot.dcim import models as dcim_models
13
14
  from nautobot.extras import models as extras_models
@@ -75,7 +76,7 @@ class PaginatorTestCase(testing.TestCase):
75
76
  warning_message = (
76
77
  "Requested &quot;per_page&quot; is too large. No more than 10 items may be displayed at a time."
77
78
  )
78
- self.assertIn(warning_message, response.content.decode(response.charset))
79
+ self.assertIn(warning_message, extract_page_body(response.content.decode(response.charset)))
79
80
  with self.subTest("query parameter per_page=5 returns 5 rows"):
80
81
  response = self.client.get(url, {"per_page": 5})
81
82
  self.assertHttpStatus(response, 200)
@@ -97,7 +98,7 @@ class PaginatorTestCase(testing.TestCase):
97
98
  warning_message = (
98
99
  "Requested &quot;per_page&quot; is too large. No more than 10 items may be displayed at a time."
99
100
  )
100
- self.assertIn(warning_message, response.content.decode(response.charset).replace("\n", ""))
101
+ self.assertIn(warning_message, extract_page_body(response.content.decode(response.charset)))
101
102
 
102
103
  @override_settings(MAX_PAGE_SIZE=0)
103
104
  def test_error_warning_not_shown_when_max_page_size_is_0(self):
@@ -118,4 +119,4 @@ class PaginatorTestCase(testing.TestCase):
118
119
  self.assertEqual(response.context["paginator"].per_page, 20)
119
120
  self.assertEqual(len(response.context["table"].page), 20)
120
121
  warning_message = "Requested &quot;per_page&quot; is too large."
121
- self.assertNotIn(warning_message, response.content.decode(response.charset))
122
+ self.assertNotIn(warning_message, extract_page_body(response.content.decode(response.charset)))
@@ -14,6 +14,7 @@ from prometheus_client.parser import text_string_to_metric_families
14
14
  from nautobot.core.constants import GLOBAL_SEARCH_EXCLUDE_LIST
15
15
  from nautobot.core.testing import TestCase
16
16
  from nautobot.core.testing.api import APITestCase
17
+ from nautobot.core.testing.utils import extract_page_body
17
18
  from nautobot.core.utils.permissions import get_permission_for_model
18
19
  from nautobot.core.views import NautobotMetricsView
19
20
  from nautobot.core.views.mixins import GetReturnURLMixin
@@ -154,16 +155,15 @@ class HomeViewTestCase(TestCase):
154
155
  BANNER_BOTTOM="[info](https://nautobot.com)",
155
156
  ):
156
157
  response = self.client.get(url)
157
- self.assertInHTML("<h1>Hello world</h1>", response.content.decode(response.charset))
158
- self.assertInHTML(
159
- '<a href="https://nautobot.com" rel="noopener noreferrer">info</a>',
160
- response.content.decode(response.charset),
158
+ self.assertBodyContains(response, "<h1>Hello world</h1>", html=True)
159
+ self.assertBodyContains(
160
+ response, '<a href="https://nautobot.com" rel="noopener noreferrer">info</a>', html=True
161
161
  )
162
162
 
163
163
  with override_settings(BANNER_LOGIN="_Welcome to Nautobot!_"):
164
164
  self.client.logout()
165
165
  response = self.client.get(reverse("login"))
166
- self.assertInHTML("<em>Welcome to Nautobot!</em>", response.content.decode(response.charset))
166
+ self.assertBodyContains(response, "<em>Welcome to Nautobot!</em>", html=True)
167
167
 
168
168
  def test_banners_no_xss(self):
169
169
  url = reverse("home")
@@ -195,21 +195,24 @@ class SearchFieldsTestCase(TestCase):
195
195
 
196
196
  # Assert model search bar present in list UI
197
197
  response = self.client.get(reverse("dcim:location_list"))
198
- self.assertInHTML(
198
+ self.assertBodyContains(
199
+ response,
199
200
  '<input type="text" name="q" class="form-control" required placeholder="Search Locations" id="id_q">',
200
- response.content.decode(response.charset),
201
+ html=True,
201
202
  )
202
203
 
203
204
  response = self.client.get(reverse("dcim:device_list"))
204
- self.assertInHTML(
205
+ self.assertBodyContains(
206
+ response,
205
207
  '<input type="text" name="q" class="form-control" required placeholder="Search Devices" id="id_q">',
206
- response.content.decode(response.charset),
208
+ html=True,
207
209
  )
208
210
 
209
211
  # Assert global search bar present in UI
210
- self.assertInHTML(
212
+ self.assertContains( # not using assertBodyContains because this is in the nav
213
+ response,
211
214
  '<input type="text" name="q" class="form-control" placeholder="Search Nautobot">',
212
- response.content.decode(response.charset),
215
+ html=True,
213
216
  )
214
217
 
215
218
 
@@ -233,16 +236,10 @@ class FilterFormsTestCase(TestCase):
233
236
  """
234
237
 
235
238
  response = self.client.get(reverse("dcim:location_list"))
236
- self.assertInHTML(
237
- filter_tabs,
238
- response.content.decode(response.charset),
239
- )
239
+ self.assertBodyContains(response, filter_tabs, html=True)
240
240
 
241
241
  response = self.client.get(reverse("circuits:circuit_list"))
242
- self.assertInHTML(
243
- filter_tabs,
244
- response.content.decode(response.charset),
245
- )
242
+ self.assertBodyContains(response, filter_tabs, html=True)
246
243
 
247
244
  def test_filtering_on_custom_select_filter_field(self):
248
245
  """Assert CustomField select and multiple select fields can be filtered using multiple entries"""
@@ -271,10 +268,8 @@ class FilterFormsTestCase(TestCase):
271
268
  )
272
269
  url = reverse("dcim:location_list") + query_param
273
270
  response = self.client.get(url)
274
- self.assertHttpStatus(response, 200)
275
- response_content = response.content.decode(response.charset).replace("\n", "")
276
- self.assertInHTML(locations[0].name, response_content)
277
- self.assertInHTML(locations[1].name, response_content)
271
+ self.assertBodyContains(response, locations[0].name, html=True)
272
+ self.assertBodyContains(response, locations[1].name, html=True)
278
273
 
279
274
  def test_filtering_crafted_query_params(self):
280
275
  """Test for reflected-XSS vulnerability GHSA-jxgr-gcj5-cqqg."""
@@ -282,17 +277,16 @@ class FilterFormsTestCase(TestCase):
282
277
  query_param = "?location_type=1 onmouseover=alert('hi') foo=bar"
283
278
  url = reverse("dcim:location_list") + query_param
284
279
  response = self.client.get(url)
285
- self.assertHttpStatus(response, 200)
286
- response_content = response.content.decode(response.charset)
287
280
  # The important thing here is that the data-field-parent and data-field-value are correctly quoted
288
- self.assertInHTML(
281
+ self.assertBodyContains(
282
+ response,
289
283
  """
290
284
  <span class="filter-selection-choice-remove remove-filter-param"
291
285
  data-field-type="child"
292
286
  data-field-parent="location_type"
293
287
  data-field-value="1 onmouseover=alert(&#x27;hi&#x27;) foo=bar"
294
288
  >×</span>""", # noqa: RUF001 - ambiguous-unicode-character-string
295
- response_content,
289
+ html=True,
296
290
  )
297
291
 
298
292
 
@@ -327,21 +321,18 @@ class NavAppsUITestCase(TestCase):
327
321
  self.url = reverse("apps:apps_list")
328
322
  self.item_weight = 100 # TODO: not easy to introspect from the nav menu struct, so hard-code it here for now
329
323
 
330
- def make_request(self):
331
- response = self.client.get(reverse("home"))
332
- return response.content.decode(response.charset)
333
-
334
324
  def test_installed_apps_visible(self):
335
325
  """The "Installed Apps" menu item should be available to an authenticated user regardless of permissions."""
336
- response_content = self.make_request()
337
- self.assertInHTML(
326
+ response = self.client.get(reverse("home"))
327
+ self.assertContains(
328
+ response,
338
329
  f"""
339
330
  <a href="{self.url}"
340
331
  data-item-weight="{self.item_weight}">
341
332
  Installed Apps
342
333
  </a>
343
334
  """,
344
- response_content,
335
+ html=True,
345
336
  )
346
337
 
347
338
 
@@ -361,7 +352,7 @@ class LoginUITestCase(TestCase):
361
352
  def make_request(self):
362
353
  response = self.client.get(reverse("login"))
363
354
  sso_login_pattern = re.compile('<a href=".*">Continue with SSO</a>')
364
- return sso_login_pattern.search(response.content.decode(response.charset))
355
+ return sso_login_pattern.search(extract_page_body(response.content.decode(response.charset)))
365
356
 
366
357
  def test_sso_login_button_not_visible(self):
367
358
  """Test Continue with SSO button not visible if SSO is enabled"""
@@ -458,13 +449,13 @@ class ErrorPagesTestCase(TestCase):
458
449
  """Nautobot's custom 404 page should be used and should include a default support message."""
459
450
  with self.assertTemplateUsed("404.html"):
460
451
  response = self.client.get("/foo/bar")
461
- self.assertContains(response, "Network to Code", status_code=404)
462
- response_content = response.content.decode(response.charset)
463
- self.assertInHTML(
452
+ self.assertBodyContains(
453
+ response,
464
454
  "If further assistance is required, please join the <code>#nautobot</code> channel on "
465
455
  '<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s '
466
456
  "Slack community</a> and post your question.",
467
- response_content,
457
+ html=True,
458
+ status_code=404,
468
459
  )
469
460
 
470
461
  @override_settings(DEBUG=False, SUPPORT_MESSAGE="Hello world!")
@@ -473,8 +464,7 @@ class ErrorPagesTestCase(TestCase):
473
464
  with self.assertTemplateUsed("404.html"):
474
465
  response = self.client.get("/foo/bar")
475
466
  self.assertNotContains(response, "Network to Code", status_code=404)
476
- response_content = response.content.decode(response.charset)
477
- self.assertInHTML("Hello world!", response_content)
467
+ self.assertBodyContains(response, "Hello world!", status_code=404)
478
468
 
479
469
  @override_settings(DEBUG=False)
480
470
  @mock.patch("nautobot.core.views.HomeView.get", side_effect=Exception)
@@ -484,13 +474,13 @@ class ErrorPagesTestCase(TestCase):
484
474
  self.client.raise_request_exception = False
485
475
  response = self.client.get(url)
486
476
  self.assertTemplateUsed(response, "500.html")
487
- self.assertContains(response, "Network to Code", status_code=500)
488
- response_content = response.content.decode(response.charset)
489
- self.assertInHTML(
477
+ self.assertBodyContains(
478
+ response,
490
479
  "If further assistance is required, please join the <code>#nautobot</code> channel on "
491
480
  '<a href="https://slack.networktocode.com/" rel="noopener noreferrer">Network to Code\'s '
492
481
  "Slack community</a> and post your question.",
493
- response_content,
482
+ html=True,
483
+ status_code=500,
494
484
  )
495
485
 
496
486
  @override_settings(DEBUG=False, SUPPORT_MESSAGE="Hello world!")
@@ -502,8 +492,7 @@ class ErrorPagesTestCase(TestCase):
502
492
  response = self.client.get(url)
503
493
  self.assertTemplateUsed(response, "500.html")
504
494
  self.assertNotContains(response, "Network to Code", status_code=500)
505
- response_content = response.content.decode(response.charset)
506
- self.assertInHTML("Hello world!", response_content)
495
+ self.assertBodyContains(response, "Hello world!", status_code=500)
507
496
 
508
497
 
509
498
  class DBFileStorageViewTestCase(TestCase):
@@ -591,23 +580,17 @@ class ExampleViewWithCustomPermissionsTest(TestCase):
591
580
  self.client.logout()
592
581
  url = reverse("plugins:example_app:view_with_custom_permissions")
593
582
  response = self.client.get(url, follow=True)
594
- self.assertHttpStatus(response, 200)
595
- response_body = response.content.decode(response.charset)
596
583
  # check if the user is redirected to the login page
597
- self.assertIn(f'<input type="hidden" name="next" value="{url}" />', response_body)
584
+ self.assertBodyContains(response, f'<input type="hidden" name="next" value="{url}" />', html=True)
598
585
 
599
586
  # Test IsAdmin permission
600
587
  self.client.force_login(self.user)
601
588
  response = self.client.get(url, follow=True)
602
- self.assertHttpStatus(response, 403)
603
- response_body = response.content.decode(response.charset)
604
589
  # check if the users have to have the permission to access the page
605
- self.assertIn("You do not have permission to access this page.", response_body)
590
+ self.assertBodyContains(response, "You do not have permission to access this page", status_code=403)
606
591
 
607
592
  # View should be successfully accessed
608
593
  self.user.is_staff = True
609
594
  self.user.save()
610
595
  response = self.client.get(url)
611
- self.assertHttpStatus(response, 200)
612
- response_body = response.content.decode(response.charset)
613
- self.assertIn("You are viewing a table of example models", response_body)
596
+ self.assertBodyContains(response, "You are viewing a table of example models")
@@ -1,3 +1,5 @@
1
+ import contextlib
2
+ import logging
1
3
  import os
2
4
  import platform
3
5
  import re
@@ -32,6 +34,7 @@ from prometheus_client import (
32
34
  )
33
35
  from prometheus_client.metrics_core import GaugeMetricFamily
34
36
  from prometheus_client.registry import Collector
37
+ import redis.exceptions
35
38
  from rest_framework.permissions import AllowAny, IsAuthenticated
36
39
  from rest_framework.renderers import BaseRenderer
37
40
  from rest_framework.response import Response
@@ -48,6 +51,8 @@ from nautobot.extras.forms import GraphQLQueryForm
48
51
  from nautobot.extras.models import FileProxy, GraphQLQuery, Status
49
52
  from nautobot.extras.registry import registry
50
53
 
54
+ logger = logging.getLogger(__name__)
55
+
51
56
 
52
57
  class HomeView(AccessMixin, TemplateView):
53
58
  template_name = "home.html"
@@ -154,26 +159,37 @@ class WorkerStatusView(UserPassesTestMixin, TemplateView):
154
159
  timeout = max_timeout
155
160
  else:
156
161
  timeout = int(timeout)
157
- celery_inspect = app.control.inspect(timeout=timeout)
158
162
 
159
- # stats() returns a dict of {worker_name: stats_dict}
160
- worker_stats = celery_inspect.stats()
163
+ celery_inspect = app.control.inspect(timeout=timeout)
161
164
 
165
+ try:
166
+ # stats() returns a dict of {worker_name: stats_dict}
167
+ worker_stats = celery_inspect.stats()
168
+ except redis.exceptions.ConnectionError:
169
+ # Celery seems to be not smart enough to auto-retry on intermittent failures, so let's do it ourselves:
170
+ try:
171
+ worker_stats = celery_inspect.stats()
172
+ except redis.exceptions.ConnectionError as err:
173
+ logger.error("Repeated ConnectionError from Celery/Redis: %s", err)
174
+ worker_stats = None
175
+
176
+ active_tasks = reserved_tasks = active_queues = {}
162
177
  if worker_stats:
163
178
  # Set explicit list of workers to speed up subsequent queries
164
179
  celery_inspect = app.control.inspect(list(worker_stats.keys()), timeout=5.0)
165
180
 
166
- # active() returns a dict of {worker_name: [task_dict, task_dict, ...]}
167
- active_tasks = celery_inspect.active() or {}
181
+ with contextlib.suppress(redis.exceptions.ConnectionError):
182
+ # active() returns a dict of {worker_name: [task_dict, task_dict, ...]}
183
+ active_tasks = celery_inspect.active() or {}
168
184
 
169
- # reserved() returns a dict of {worker_name: [task_dict, task_dict, ...]}
170
- reserved_tasks = celery_inspect.reserved() or {}
185
+ # reserved() returns a dict of {worker_name: [task_dict, task_dict, ...]}
186
+ reserved_tasks = celery_inspect.reserved() or {}
171
187
 
172
- # active_queues() returns a dict of {worker_name: [queue_dict, queue_dict, ...]}
173
- active_queues = celery_inspect.active_queues() or {}
188
+ # active_queues() returns a dict of {worker_name: [queue_dict, queue_dict, ...]}
189
+ active_queues = celery_inspect.active_queues() or {}
174
190
  else:
175
- # No workers were found, default to empty dicts for all commands
176
- worker_stats = active_tasks = reserved_tasks = active_queues = {}
191
+ # No workers were found
192
+ worker_stats = {}
177
193
 
178
194
  workers = []
179
195
  for worker_name in sorted(worker_stats, key=sort_workers):