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
nautobot/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from importlib import metadata
2
2
  import logging
3
3
  import os
4
+ import sys
4
5
 
5
6
  import django
6
7
 
@@ -28,8 +29,9 @@ def setup(config_path=None):
28
29
  # Point Django to our 'nautobot_config' pseudo-module that we'll load from the provided config path
29
30
  os.environ["DJANGO_SETTINGS_MODULE"] = "nautobot_config"
30
31
 
31
- load_settings(config_path)
32
+ if "nautobot_config" not in sys.modules:
33
+ load_settings(config_path)
32
34
  django.setup()
33
35
 
34
- logger.info("Nautobot initialized!")
36
+ logger.info("Nautobot %s initialized!", __version__)
35
37
  __initialized = True
@@ -10,7 +10,7 @@ from nautobot.circuits.models import (
10
10
  Provider,
11
11
  ProviderNetwork,
12
12
  )
13
- from nautobot.core.testing import post_data, TestCase as NautobotTestCase, ViewTestCases
13
+ from nautobot.core.testing import post_data, TestCase as NautobotTestCase, utils, ViewTestCases
14
14
  from nautobot.extras.models import Status, Tag
15
15
 
16
16
 
@@ -176,13 +176,12 @@ class CircuitTerminationTestCase(
176
176
 
177
177
  # Visit the termination detail page and assert responses:
178
178
  response = self.client.get(reverse("circuits:circuittermination", kwargs={"pk": termination.pk}))
179
- self.assertEqual(200, response.status_code)
180
- self.assertIn("Test Provider Network", str(response.content))
181
- self.assertNotIn("</span> Connect", str(response.content))
179
+ self.assertBodyContains(response, "Test Provider Network")
180
+ self.assertNotIn("</span> Connect", utils.extract_page_body(response.content.decode(response.charset)))
182
181
 
183
182
  # Visit the circuit object detail page and check there is no connect button present:
184
183
  response = self.client.get(reverse("circuits:circuit", kwargs={"pk": circuit.pk}))
185
- self.assertNotIn("</span> Connect", str(response.content))
184
+ self.assertNotIn("</span> Connect", utils.extract_page_body(response.content.decode(response.charset)))
186
185
 
187
186
 
188
187
  class CircuitSwapTerminationsTestCase(NautobotTestCase):
@@ -24,6 +24,7 @@ from graphql import get_default_backend
24
24
  from graphql.execution import ExecutionResult
25
25
  from graphql.execution.middleware import MiddlewareManager
26
26
  from graphql.type.schema import GraphQLSchema
27
+ import redis.exceptions
27
28
  from rest_framework import routers, status
28
29
  from rest_framework.exceptions import ParseError, PermissionDenied
29
30
  from rest_framework.permissions import IsAuthenticated
@@ -58,6 +59,10 @@ HTTP_ACTIONS = {
58
59
  "DELETE": "delete",
59
60
  }
60
61
 
62
+
63
+ logger = logging.getLogger(__name__)
64
+
65
+
61
66
  #
62
67
  # Mixins
63
68
  #
@@ -467,7 +472,16 @@ class StatusView(NautobotAPIVersionMixin, APIView):
467
472
  nautobot_apps = dict(sorted(nautobot_apps.items()))
468
473
 
469
474
  # Gather Celery workers
470
- workers = celery_app.control.inspect().active() # list or None
475
+ try:
476
+ workers = celery_app.control.inspect().active() # list or None
477
+ except redis.exceptions.ConnectionError:
478
+ # Celery seems to be not smart enough to auto-retry on intermittent failures, so let's do it ourselves:
479
+ try:
480
+ workers = celery_app.control.inspect().active() # list or None
481
+ except redis.exceptions.ConnectionError as err:
482
+ logger.error("Repeated ConnectionError from Celery/Redis: %s", err)
483
+ workers = None
484
+
471
485
  worker_count = len(workers) if workers is not None else 0
472
486
 
473
487
  return Response(
@@ -872,7 +886,6 @@ class GetObjectCountsView(NautobotAPIVersionMixin, APIView):
872
886
  try:
873
887
  data["url"] = django_reverse(get_route_for_model(model, "list"))
874
888
  except NoReverseMatch:
875
- logger = logging.getLogger(__name__)
876
889
  route = get_route_for_model(model, "list")
877
890
  logger.warning(f"Handled expected exception when generating filter field: {route}")
878
891
  manager = model.objects
@@ -975,7 +988,6 @@ class GetFilterSetFieldDOMElementAPIView(NautobotAPIVersionMixin, APIView):
975
988
  # Cant determine the exceptions to handle because any exception could be raised,
976
989
  # e.g InterfaceForm would raise a ObjectDoesNotExist Error since no device was provided
977
990
  # While other forms might raise other errors, also if model_form is None a TypeError would be raised.
978
- logger = logging.getLogger(__name__)
979
991
  logger.debug(f"Handled expected exception when generating filter field: {err}")
980
992
 
981
993
  # Create a temporary form and get a BoundField for the specified field
@@ -18,6 +18,8 @@
18
18
  onerror="window.location='{% url 'media_failure' %}?filename=highlight.js-11.9.0/highlight.min.js'"></script>
19
19
  <script src="{% versioned_static 'js/forms.js' %}"
20
20
  onerror="window.location='{% url 'media_failure' %}?filename=js/forms.js'"></script>
21
+ <script src="{% versioned_static 'js/nav_menu.js' %}"
22
+ onerror="window.location='{% url 'media_failure' %}?filename=js/nav_menu.js'"></script>
21
23
  <script src="{% versioned_static 'js/theme.js' %}"
22
24
  onerror="window.location='{% url 'media_failure' %}?filename=js/theme.js'"></script>
23
25
  <script src="{% versioned_static 'js/table_sorting_indicator.js' %}"
@@ -117,254 +117,3 @@
117
117
  <button type="button" class="btn btn-xs btn-warning navbar-toggler" aria-label="Collapse navbar">
118
118
  <span class="mdi mdi-chevron-up mdi-rotate-270 navbar-toggler-arrow"></span>
119
119
  </button>
120
-
121
- <script>
122
- document.addEventListener('DOMContentLoaded', function() {
123
- const navbar = document.querySelector('.navbar-fixed-left');
124
- const navbarHeader = document.querySelector('.navbar-header');
125
- const mainContent = document.querySelector('#main-content');
126
- const footer = document.querySelector('#footer');
127
- const dropdownToggles = document.querySelectorAll('.navbar-fixed-left .navbar-nav > .dropdown > a[data-toggle="collapse"]');
128
- const dropdowns = document.querySelectorAll('.navbar-fixed-left .navbar-nav .collapse');
129
- const toggler = document.querySelector('.navbar-toggler');
130
- const togglerIcon = toggler.querySelector('.navbar-toggler-arrow');
131
- let lastDropdownId = sessionStorage.getItem('lastOpenedDropdown');
132
- let savedScrollPosition = sessionStorage.getItem('navbarScrollPosition');
133
- let activeLink = sessionStorage.getItem('activeLink');
134
- let expandedByHover = false;
135
- let manuallyToggled = sessionStorage.getItem('manuallyToggled') === 'true';
136
-
137
- // Function to reset stored dropdown state information
138
- function resetNavbarState() {
139
- sessionStorage.removeItem('lastOpenedDropdown');
140
- sessionStorage.removeItem('savedScrollPosition');
141
- sessionStorage.removeItem('activeLink');
142
- sessionStorage.removeItem('navbarCollapsed');
143
- expandedByHover = false;
144
- }
145
-
146
- toggler.addEventListener('click', function() {
147
- let isNowCollapsed;
148
- if (expandedByHover) {
149
- expandedByHover = false;
150
- isNowCollapsed = false;
151
- } else {
152
- isNowCollapsed = navbar.classList.toggle('collapsed');
153
- }
154
- sessionStorage.setItem('navbarCollapsed', isNowCollapsed ? 'true' : 'false');
155
- // Set 'navbarManuallyToggled' to track any manual toggle
156
- sessionStorage.setItem('navbarManuallyToggled', 'true');
157
- // Track if the action was an expansion or a collapse
158
- sessionStorage.setItem('navbarExpanded', !isNowCollapsed ? 'true' : 'false');
159
- if (isNowCollapsed) {
160
- togglerIcon.classList.add("mdi-rotate-90");
161
- togglerIcon.classList.remove("mdi-rotate-270");
162
- } else {
163
- togglerIcon.classList.remove("mdi-rotate-90");
164
- togglerIcon.classList.add("mdi-rotate-270");
165
- }
166
- adjustElementsForNavbarState(isNowCollapsed);
167
- });
168
-
169
- // Retrieve the navbar collapsed state from session storage on page load
170
- const navbarCollapsed = sessionStorage.getItem('navbarCollapsed') === 'true';
171
- if (navbarCollapsed) {
172
- navbar.classList.add('collapsed');
173
- togglerIcon.classList.remove("mdi-rotate-270");
174
- togglerIcon.classList.add("mdi-rotate-90");
175
- adjustElementsForNavbarState(true);
176
- }
177
-
178
- function adjustElementsForNavbarState(isCollapsed) {
179
- const marginLeftValue = isCollapsed ? '-240px' : '0px';
180
- mainContent.style.marginLeft = marginLeftValue;
181
- if(footer) footer.style.marginLeft = marginLeftValue;
182
- toggler.style.left = isCollapsed ? '-5px' : '225px';
183
- }
184
-
185
- // Expand navbar when hovering near the left edge of the screen
186
- document.addEventListener('mousemove', function(e) {
187
- if (
188
- e.clientX < 20 // 20px from the left edge
189
- && (e.clientY < 20 || e.clientY > 50) // not near the toggle button
190
- && navbar.classList.contains('collapsed')
191
- ) {
192
- navbar.classList.remove('collapsed');
193
- toggler.style.left = '225px';
194
- expandedByHover = true; // Set flag when expanded by hover
195
- } else if (expandedByHover && e.clientX > 240) {
196
- navbar.classList.add('collapsed');
197
- toggler.style.left = '-5px';
198
- expandedByHover = false; // Reset flag after auto-collapse
199
- }
200
- });
201
-
202
- function collapseNavbarIfNeeded() {
203
- const windowWidth = window.innerWidth;
204
- const navbarManuallyToggled = sessionStorage.getItem('navbarManuallyToggled') === 'true';
205
- const navbarExpanded = sessionStorage.getItem('navbarExpanded') === 'true';
206
- const isCollapsed = navbar.classList.contains('collapsed');
207
-
208
- if (windowWidth < 1007) {
209
- if (!isCollapsed) {
210
- navbar.classList.add('collapsed');
211
- togglerIcon.classList.remove("mdi-rotate-270");
212
- togglerIcon.classList.add("mdi-rotate-90");
213
- adjustElementsForNavbarState(true);
214
- sessionStorage.setItem('navbarCollapsed', 'true');
215
- }
216
- } else if (windowWidth >= 1007) {
217
- // Only expand automatically if it was not manually collapsed
218
- if (isCollapsed && (navbarManuallyToggled && navbarExpanded)) {
219
- navbar.classList.remove('collapsed');
220
- togglerIcon.classList.add("mdi-rotate-270");
221
- togglerIcon.classList.remove("mdi-rotate-90");
222
- adjustElementsForNavbarState(false);
223
- sessionStorage.setItem('navbarCollapsed', 'false');
224
- }
225
- // Do not automatically change the state if it was manually collapsed
226
- }
227
- }
228
-
229
- // Update the window resize listener
230
- function toggleNavbarOnResize() {
231
- collapseNavbarIfNeeded(); // Use the new function to decide whether to collapse
232
- }
233
-
234
- let debouncedToggleNavbarOnResize = debounce(toggleNavbarOnResize, 50);
235
- window.addEventListener('resize', debouncedToggleNavbarOnResize);
236
-
237
- // Select the navbar dropdown elements
238
- let navbarItems = document.querySelectorAll('.navbar-fixed-left .navbar-nav > .dropdown > .dropdown-toggle > #dropdown_title');
239
-
240
- // Add a title attribute and tooltip, only if necessary
241
- navbarItems.forEach(function(item) {
242
- // Check if the text overflows
243
- if (item.scrollWidth > item.clientWidth) {
244
- // Set the title attribute
245
- item.setAttribute('title', item.innerText);
246
-
247
- // Reinitialize Bootstrap tooltip
248
- $(item).tooltip();
249
- }
250
- });
251
-
252
- // Add an event listener for the home link click
253
- const homeLink = document.querySelector('.navbar-fixed-left .navbar-brand');
254
- if (homeLink) {
255
- homeLink.addEventListener('click', function() {
256
- resetNavbarState();
257
- });
258
- }
259
-
260
- // Close all dropdowns except the one specified
261
- function closeAllDropdownsExcept(exceptId) {
262
- dropdowns.forEach(function(collapse) {
263
- if (collapse.id !== exceptId && collapse.classList.contains('in')) {
264
- $(collapse).collapse('hide');
265
- }
266
- });
267
- }
268
-
269
- // Add click event listener to the dropdown links and save the clicked one
270
- function addLinkClickListeners() {
271
- const dropdownLinks = document.querySelectorAll('.navbar-fixed-left .navbar-nav > .dropdown > .nav-dropdown-menu > li > a');
272
-
273
- dropdownLinks.forEach(function(link) {
274
- link.addEventListener('click', function() {
275
- sessionStorage.setItem('activeLink', link.getAttribute('href'));
276
- });
277
- });
278
- collapseNavbarIfNeeded();
279
- }
280
-
281
- // Close all dropdowns except the last opened one
282
- dropdownToggles.forEach(function(toggle) {
283
- toggle.addEventListener('click', function(event) {
284
- event.preventDefault();
285
- const collapseElement = document.getElementById(this.getAttribute('href').substring(1));
286
-
287
- if (!collapseElement.classList.contains('in')) {
288
- closeAllDropdownsExcept(collapseElement.id);
289
- $(collapseElement).collapse('show');
290
- sessionStorage.setItem('lastOpenedDropdown', collapseElement.id);
291
- } else {
292
- $(collapseElement).collapse('hide');
293
- sessionStorage.removeItem('lastOpenedDropdown');
294
- }
295
- });
296
- });
297
-
298
- // Open the last opened dropdown
299
- if (lastDropdownId) {
300
- let lastDropdownMenu = document.getElementById(lastDropdownId);
301
- if (lastDropdownMenu && !lastDropdownMenu.classList.contains('in')) {
302
- $(lastDropdownMenu).collapse('show');
303
- }
304
- }
305
-
306
- // Restore the last saved scroll position
307
- if (savedScrollPosition) {
308
- navbar.scrollTop = savedScrollPosition;
309
- }
310
-
311
- // Function to adjust navbar header visibility based on scroll position and navbar collapsed state
312
- function adjustNavbarHeaderVisibility() {
313
- // Check if the navbar is collapsed and mainContent is defined
314
- if (navbar.classList.contains('collapsed') && mainContent) {
315
- const mainContentTop = mainContent.getBoundingClientRect().top;
316
- // Show or hide the navbar header based on mainContent's position
317
- if (mainContentTop < 0) {
318
- // Main content top is out of view, hide navbar header
319
- navbarHeader.style.top = '-60px'; // height of navbar header
320
- } else {
321
- // Main content top is in view, show navbar header
322
- navbarHeader.style.top = '0';
323
- }
324
- }
325
- }
326
-
327
- // Add scroll event listener to adjust navbar header visibility
328
- window.addEventListener('scroll', adjustNavbarHeaderVisibility);
329
-
330
- // Call the function initially to set the correct state when the page loads
331
- adjustNavbarHeaderVisibility();
332
-
333
- // Debounce function to limit the rate at which the handleScroll function is executed
334
- function debounce(func, wait, immediate) {
335
- let timeout;
336
- return function() {
337
- const context = this, args = arguments;
338
- const later = function() {
339
- timeout = null;
340
- if (!immediate) func.apply(context, args);
341
- };
342
- const callNow = immediate && !timeout;
343
- clearTimeout(timeout);
344
- timeout = setTimeout(later, wait);
345
- if (callNow) func.apply(context, args);
346
- };
347
- }
348
-
349
- // Save the scroll position when the navbar is scrolled
350
- navbar.addEventListener('scroll', debounce(function() {
351
- sessionStorage.setItem('navbarScrollPosition', navbar.scrollTop);
352
- }, 250));
353
-
354
- // Add click event listeners to dropdown links
355
- addLinkClickListeners();
356
-
357
- // Apply the 'active' class to the previously clicked link
358
- if (activeLink) {
359
- let previouslyClickedLink = document.querySelector('.navbar-fixed-left .navbar-nav > .dropdown > .nav-dropdown-menu > li > a[href="' + activeLink + '"]');
360
- let currentLocation = window.location.pathname + window.location.search;
361
-
362
- if (previouslyClickedLink && currentLocation.includes(previouslyClickedLink.getAttribute('href'))) {
363
- previouslyClickedLink.parentElement.classList.add('active');
364
- }
365
- else {
366
- sessionStorage.removeItem('activeLink');
367
- }
368
- }
369
- });
370
- </script>
@@ -9,6 +9,7 @@ from django.core.exceptions import FieldDoesNotExist
9
9
  from django.db import connections, DEFAULT_DB_ALIAS
10
10
  from django.db.models import JSONField, ManyToManyField, ManyToManyRel
11
11
  from django.forms.models import model_to_dict
12
+ from django.test.testcases import assert_and_parse_html
12
13
  from django.test.utils import CaptureQueriesContext
13
14
  from netaddr import IPNetwork
14
15
  from rest_framework.test import APIClient, APIRequestFactory
@@ -172,8 +173,12 @@ class NautobotTestCaseMixin:
172
173
  # REST API response; pass the response data through directly
173
174
  err_message += f"\n{response.data}"
174
175
  # Attempt to extract form validation errors from the response HTML
175
- form_errors = utils.extract_form_failures(response.content.decode(response.charset))
176
- err_message += "\n" + str(form_errors or response.content.decode(response.charset) or "No data")
176
+ elif form_errors := utils.extract_form_failures(response.content.decode(response.charset)):
177
+ err_message += f"\n{form_errors}"
178
+ elif body_content := utils.extract_page_body(response.content.decode(response.charset)):
179
+ err_message += f"\n{body_content}"
180
+ else:
181
+ err_message += "No data"
177
182
  if msg:
178
183
  err_message = f"{msg}\n{err_message}"
179
184
  self.assertIn(response.status_code, expected_status, err_message)
@@ -277,6 +282,58 @@ class NautobotTestCaseMixin:
277
282
 
278
283
  return None
279
284
 
285
+ def assertBodyContains(self, response, text, count=None, status_code=200, msg_prefix="", html=False):
286
+ """
287
+ Like Django's `assertContains`, but uses `extract_page_body` utility function to scope the check more narrowly.
288
+
289
+ Args:
290
+ response (HttpResponse): The response to inspect
291
+ text (str): Plaintext or HTML to check for in the response body
292
+ count (int, optional): Number of times the `text` should occur, or None if we don't care as long as
293
+ it's present at all.
294
+ status_code (int): HTTP status code expected
295
+ html (bool): If True, handle `text` as HTML, ignoring whitespace etc, as in Django's `assertHTMLEqual()`.
296
+ """
297
+ # The below is copied from SimpleTestCase._assert_contains and SimpleTestCase.assertContains
298
+ # If the response supports deferred rendering and hasn't been rendered
299
+ # yet, then ensure that it does get rendered before proceeding further.
300
+ if hasattr(response, "render") and callable(response.render) and not response.is_rendered:
301
+ response.render()
302
+
303
+ if msg_prefix:
304
+ msg_prefix += ": "
305
+
306
+ self.assertHttpStatus( # Nautobot-specific, original uses simple assertEqual()
307
+ response, status_code, msg_prefix
308
+ )
309
+
310
+ if response.streaming:
311
+ content = b"".join(response.streaming_content)
312
+ else:
313
+ content = response.content
314
+
315
+ if not isinstance(text, bytes) or html:
316
+ text = str(text)
317
+ content = content.decode(response.charset)
318
+ content = utils.extract_page_body(content) # Nautobot-specific
319
+ text_repr = f"'{text}'"
320
+ else:
321
+ text_repr = repr(text)
322
+
323
+ if html:
324
+ content = assert_and_parse_html(self, content, None, "Response's content is not valid HTML:")
325
+ text = assert_and_parse_html(self, text, None, "Second argument is not valid HTML:")
326
+ real_count = content.count(text)
327
+
328
+ if count is not None:
329
+ self.assertEqual(
330
+ real_count,
331
+ count,
332
+ msg_prefix + f"Found {real_count} instances of {text_repr} in response (expected {count}):\n{content}",
333
+ )
334
+ else:
335
+ self.assertTrue(real_count != 0, msg_prefix + f"Couldn't find {text_repr} in response:\n{content}")
336
+
280
337
  #
281
338
  # Convenience methods
282
339
  #