nautobot 2.3.5__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 (337) hide show
  1. nautobot/__init__.py +4 -2
  2. nautobot/circuits/tests/test_views.py +4 -5
  3. nautobot/core/api/utils.py +12 -2
  4. nautobot/core/api/views.py +15 -3
  5. nautobot/core/forms/fields.py +5 -2
  6. nautobot/core/forms/utils.py +31 -6
  7. nautobot/core/models/fields.py +56 -0
  8. nautobot/core/templates/inc/javascript.html +2 -0
  9. nautobot/core/templates/inc/nav_menu.html +0 -251
  10. nautobot/core/testing/mixins.py +59 -2
  11. nautobot/core/testing/views.py +45 -61
  12. nautobot/core/tests/runner.py +6 -3
  13. nautobot/core/tests/test_paginator.py +4 -3
  14. nautobot/core/tests/test_utils.py +83 -0
  15. nautobot/core/tests/test_views.py +39 -56
  16. nautobot/core/views/__init__.py +27 -11
  17. nautobot/dcim/tests/test_api.py +4 -1
  18. nautobot/dcim/tests/test_views.py +26 -67
  19. nautobot/extras/datasources/git.py +6 -1
  20. nautobot/extras/factory.py +2 -1
  21. nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +3 -0
  22. nautobot/extras/migrations/0116_fix_dynamic_group_group_type_data_migration.py +16 -0
  23. nautobot/extras/models/models.py +2 -0
  24. nautobot/extras/tests/test_api.py +3 -3
  25. nautobot/extras/tests/test_customfields.py +9 -16
  26. nautobot/extras/tests/test_dynamicgroups.py +116 -0
  27. nautobot/extras/tests/test_forms.py +2 -0
  28. nautobot/extras/tests/test_plugins.py +4 -6
  29. nautobot/extras/tests/test_utils.py +5 -0
  30. nautobot/extras/tests/test_views.py +63 -161
  31. nautobot/extras/utils.py +50 -11
  32. nautobot/ipam/api/serializers.py +30 -1
  33. nautobot/ipam/api/views.py +165 -3
  34. nautobot/ipam/filters.py +1 -1
  35. nautobot/ipam/forms.py +2 -0
  36. nautobot/ipam/migrations/0050_vlangroup_range.py +24 -0
  37. nautobot/ipam/models.py +51 -8
  38. nautobot/ipam/tables.py +4 -4
  39. nautobot/ipam/templates/ipam/vlangroup.html +4 -0
  40. nautobot/ipam/tests/test_api.py +192 -12
  41. nautobot/ipam/tests/test_models.py +35 -1
  42. nautobot/ipam/tests/test_utils.py +61 -0
  43. nautobot/ipam/tests/test_views.py +8 -15
  44. nautobot/ipam/utils/__init__.py +10 -17
  45. nautobot/ipam/views.py +1 -1
  46. nautobot/project-static/docs/404.html +3 -3
  47. nautobot/project-static/docs/apps/index.html +3 -3
  48. nautobot/project-static/docs/apps/nautobot-apps.html +3 -3
  49. nautobot/project-static/docs/assets/javascripts/bundle.525ec568.min.js +16 -0
  50. nautobot/project-static/docs/assets/javascripts/{bundle.56dfad97.min.js.map → bundle.525ec568.min.js.map} +4 -4
  51. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +1 -0
  52. nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +1 -0
  53. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +3 -3
  54. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +3 -3
  55. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3 -3
  56. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +3 -3
  57. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +3 -3
  58. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +3 -3
  59. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +3 -3
  60. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +3 -3
  61. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +3 -3
  62. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +3 -3
  63. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +3 -3
  64. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +5 -5
  65. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
  66. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
  67. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
  68. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
  69. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
  70. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
  71. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +124 -3
  72. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +3 -3
  73. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +3 -3
  74. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +3 -3
  75. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3 -3
  76. nautobot/project-static/docs/development/apps/api/configuration-view.html +3 -3
  77. nautobot/project-static/docs/development/apps/api/database-backend-config.html +3 -3
  78. nautobot/project-static/docs/development/apps/api/models/django-admin.html +3 -3
  79. nautobot/project-static/docs/development/apps/api/models/global-search.html +3 -3
  80. nautobot/project-static/docs/development/apps/api/models/graphql.html +3 -3
  81. nautobot/project-static/docs/development/apps/api/models/index.html +5 -5
  82. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +3 -3
  85. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
  86. nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
  87. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
  88. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
  89. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
  90. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
  91. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
  92. nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
  93. nautobot/project-static/docs/development/apps/api/setup.html +3 -3
  94. nautobot/project-static/docs/development/apps/api/testing.html +3 -3
  95. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
  96. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
  97. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
  98. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
  99. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
  100. nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
  101. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
  102. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
  103. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
  104. nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
  105. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
  106. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
  107. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
  108. nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
  109. nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
  110. nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
  111. nautobot/project-static/docs/development/apps/index.html +3 -3
  112. nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
  113. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
  114. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  115. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
  116. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
  117. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
  118. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
  119. nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
  120. nautobot/project-static/docs/development/core/application-registry.html +3 -3
  121. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  122. nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
  123. nautobot/project-static/docs/development/core/caching.html +3 -3
  124. nautobot/project-static/docs/development/core/controllers.html +3 -3
  125. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
  126. nautobot/project-static/docs/development/core/generic-views.html +3 -3
  127. nautobot/project-static/docs/development/core/getting-started.html +3 -3
  128. nautobot/project-static/docs/development/core/homepage.html +3 -3
  129. nautobot/project-static/docs/development/core/index.html +3 -3
  130. nautobot/project-static/docs/development/core/model-checklist.html +3 -3
  131. nautobot/project-static/docs/development/core/model-features.html +3 -3
  132. nautobot/project-static/docs/development/core/natural-keys.html +3 -3
  133. nautobot/project-static/docs/development/core/navigation-menu.html +3 -3
  134. nautobot/project-static/docs/development/core/release-checklist.html +3 -3
  135. nautobot/project-static/docs/development/core/role-internals.html +3 -3
  136. nautobot/project-static/docs/development/core/settings.html +3 -3
  137. nautobot/project-static/docs/development/core/style-guide.html +3 -3
  138. nautobot/project-static/docs/development/core/templates.html +3 -3
  139. nautobot/project-static/docs/development/core/testing.html +3 -3
  140. nautobot/project-static/docs/development/core/user-preferences.html +3 -3
  141. nautobot/project-static/docs/development/index.html +3 -3
  142. nautobot/project-static/docs/development/jobs/index.html +3 -3
  143. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -3
  144. nautobot/project-static/docs/index.html +3 -3
  145. nautobot/project-static/docs/objects.inv +0 -0
  146. nautobot/project-static/docs/overview/application_stack.html +3 -3
  147. nautobot/project-static/docs/overview/design_philosophy.html +3 -3
  148. nautobot/project-static/docs/release-notes/index.html +3 -3
  149. nautobot/project-static/docs/release-notes/version-1.0.html +3 -3
  150. nautobot/project-static/docs/release-notes/version-1.1.html +3 -3
  151. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  152. nautobot/project-static/docs/release-notes/version-1.3.html +3 -3
  153. nautobot/project-static/docs/release-notes/version-1.4.html +3 -3
  154. nautobot/project-static/docs/release-notes/version-1.5.html +3 -3
  155. nautobot/project-static/docs/release-notes/version-1.6.html +3 -3
  156. nautobot/project-static/docs/release-notes/version-2.0.html +3 -3
  157. nautobot/project-static/docs/release-notes/version-2.1.html +3 -3
  158. nautobot/project-static/docs/release-notes/version-2.2.html +3 -3
  159. nautobot/project-static/docs/release-notes/version-2.3.html +392 -95
  160. nautobot/project-static/docs/requirements.txt +1 -1
  161. nautobot/project-static/docs/search/search_index.json +1 -1
  162. nautobot/project-static/docs/sitemap.xml +269 -269
  163. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  164. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  165. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +3 -3
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +3 -3
  167. nautobot/project-static/docs/user-guide/administration/configuration/index.html +3 -3
  168. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +3 -3
  169. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -3
  170. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +3 -3
  171. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
  172. nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -3
  173. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +3 -3
  175. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +3 -3
  176. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +3 -3
  177. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +3 -3
  178. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +3 -3
  179. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +3 -3
  180. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +3 -3
  181. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +3 -3
  182. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -3
  183. nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
  184. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +3 -3
  185. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +3 -3
  186. nautobot/project-static/docs/user-guide/administration/installation/services.html +3 -3
  187. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +3 -3
  188. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  189. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +3 -3
  190. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +3 -3
  191. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +3 -3
  192. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +3 -3
  193. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +3 -3
  194. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +3 -3
  195. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +3 -3
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +3 -3
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +3 -3
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +3 -3
  199. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +3 -3
  200. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +3 -3
  201. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +3 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +3 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +3 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +3 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +3 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +3 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +3 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +3 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +3 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +3 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +3 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +3 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +3 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +3 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +3 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +3 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +3 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +3 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +3 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +3 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +3 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +3 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +3 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +3 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +3 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +3 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +3 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +3 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +3 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +3 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +3 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +3 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +3 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +3 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +3 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +3 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +3 -3
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +3 -3
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +3 -3
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +3 -3
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +3 -3
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +3 -3
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +3 -3
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +3 -3
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +3 -3
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +3 -3
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +3 -3
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +3 -3
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +3 -3
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +3 -3
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +3 -3
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +3 -3
  253. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +3 -3
  254. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +3 -3
  255. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +3 -3
  256. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +3 -3
  257. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +3 -3
  258. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +3 -3
  259. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +3 -3
  260. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +3 -3
  261. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +3 -3
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +3 -3
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +3 -3
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +308 -3
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
  266. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
  267. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
  268. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
  269. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
  270. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
  271. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
  272. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
  273. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
  274. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
  275. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  276. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +3 -3
  277. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
  278. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
  279. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +3 -3
  280. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
  281. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
  282. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
  283. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
  284. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +3 -3
  285. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
  286. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
  287. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
  288. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
  289. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
  290. nautobot/project-static/docs/user-guide/index.html +3 -3
  291. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
  292. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
  293. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
  294. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
  295. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
  296. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
  297. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
  298. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
  299. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
  300. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
  301. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
  302. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
  303. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +3 -3
  304. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
  305. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
  306. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +3 -3
  307. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +3 -3
  308. nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
  309. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +3 -3
  310. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +3 -3
  311. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +3 -3
  312. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +3 -3
  313. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +3 -3
  314. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +3 -3
  315. nautobot/project-static/docs/user-guide/platform-functionality/role.html +3 -3
  316. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +3 -3
  317. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
  318. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +3 -3
  319. nautobot/project-static/docs/user-guide/platform-functionality/status.html +3 -3
  320. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +3 -3
  321. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +3 -3
  322. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +3 -3
  323. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +3 -3
  324. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +3 -3
  325. nautobot/project-static/js/nav_menu.js +249 -0
  326. nautobot/tenancy/templates/tenancy/tenant.html +1 -1
  327. nautobot/users/tests/test_views.py +9 -11
  328. nautobot/virtualization/tests/test_views.py +3 -5
  329. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/METADATA +2 -1
  330. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/RECORD +334 -330
  331. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/WHEEL +1 -1
  332. nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +0 -16
  333. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +0 -1
  334. nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +0 -1
  335. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/LICENSE.txt +0 -0
  336. {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/NOTICE +0 -0
  337. {nautobot-2.3.5.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):
@@ -116,7 +116,7 @@ def get_serializer_for_model(model, prefix=""):
116
116
  return dynamic_import(serializer_name)
117
117
  except AttributeError as exc:
118
118
  raise exceptions.SerializerNotFound(
119
- f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'"
119
+ f"Serializer for {app_label}.{model_name} not found, expected it at {serializer_name}"
120
120
  ) from exc
121
121
 
122
122
 
@@ -129,16 +129,26 @@ def nested_serializers_for_models(models, prefix=""):
129
129
 
130
130
  Used exclusively in OpenAPI schema generation.
131
131
  """
132
+ from nautobot.core.api.serializers import BaseModelSerializer # avoid circular import
133
+
132
134
  serializer_classes = []
133
135
  for model in models:
134
136
  try:
135
137
  serializer_classes.append(get_serializer_for_model(model, prefix=prefix))
136
138
  except exceptions.SerializerNotFound as exc:
137
- logger.error("%s", exc)
139
+ logger.warning("%s", exc)
138
140
  continue
139
141
 
140
142
  nested_serializer_classes = []
141
143
  for serializer_class in serializer_classes:
144
+ if not issubclass(serializer_class, BaseModelSerializer):
145
+ logger.warning(
146
+ "Serializer class %s.%s does not inherit from nautobot.apps.api.BaseModelSerializer. "
147
+ "This should probably be corrected.",
148
+ serializer_class.__module__,
149
+ serializer_class.__name__,
150
+ )
151
+ continue
142
152
  nested_serializer_name = f"Nested{serializer_class.__name__}"
143
153
  if nested_serializer_name in NESTED_SERIALIZER_CACHE:
144
154
  nested_serializer_classes.append(NESTED_SERIALIZER_CACHE[nested_serializer_name])
@@ -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
@@ -719,8 +719,11 @@ class NumericArrayField(SimpleArrayField):
719
719
 
720
720
  def to_python(self, value):
721
721
  try:
722
- value = ",".join([str(n) for n in forms.parse_numeric_range(value)])
723
- except ValueError as error:
722
+ if not value:
723
+ value = ""
724
+ else:
725
+ value = ",".join([str(n) for n in forms.parse_numeric_range(value)])
726
+ except (TypeError, ValueError) as error:
724
727
  raise ValidationError(error)
725
728
  return super().to_python(value)
726
729
 
@@ -1,3 +1,4 @@
1
+ from itertools import groupby
1
2
  import re
2
3
 
3
4
  from django import forms as django_forms
@@ -18,24 +19,31 @@ __all__ = (
18
19
  )
19
20
 
20
21
 
21
- def parse_numeric_range(string, base=10):
22
+ def parse_numeric_range(input_string, base=10):
22
23
  """
23
- Expand a numeric range (continuous or not) into a decimal or
24
+ Expand a numeric range (continuous or not) into a sorted decimal or
24
25
  hexadecimal list, as specified by the base parameter
25
26
  '0-3,5' => [0, 1, 2, 3, 5]
26
27
  '2,8-b,d,f' => [2, 8, 9, a, b, d, f]
27
28
  """
29
+ if base not in [10, 16]:
30
+ raise TypeError("Invalid base value.")
31
+
32
+ if not isinstance(input_string, str) or not input_string:
33
+ raise TypeError("Input value must be a string using a range format.")
34
+
28
35
  values = []
29
- if not string:
30
- return values
31
- for dash_range in string.split(","):
36
+
37
+ for dash_range in input_string.split(","):
32
38
  try:
33
39
  begin, end = dash_range.split("-")
40
+ if begin == "" or end == "":
41
+ raise TypeError("Input value must be a string using a range format.")
34
42
  except ValueError:
35
43
  begin, end = dash_range, dash_range
36
44
  begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
37
45
  values.extend(range(begin, end))
38
- return list(set(values))
46
+ return sorted(list(set(values)))
39
47
 
40
48
 
41
49
  def parse_alphanumeric_range(string):
@@ -150,3 +158,20 @@ def add_field_to_filter_form_class(form_class, field_name, field_obj):
150
158
  f"There was a conflict with filter form field `{field_name}`, the custom filter form field was ignored."
151
159
  )
152
160
  form_class.base_fields[field_name] = field_obj
161
+
162
+
163
+ def compress_range(iterable):
164
+ """
165
+ Generates compressed range from an un-sorted expanded range.
166
+ For example:
167
+ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 101, 102, 103, 104, 105, 1000, 1100, 1101, 1102, 1103, 1104, 1105, 1106]
168
+ =>
169
+ iter1: (1, 10)
170
+ iter2: (100, 105)
171
+ iter3: (1000, 1000)
172
+ iter4: (1100, 1106)
173
+ """
174
+ iterable = sorted(set(iterable))
175
+ for _, grp in groupby(enumerate(iterable), lambda t: t[1] - t[0]):
176
+ grp = list(grp)
177
+ yield grp[0][1], grp[-1][1]
@@ -4,6 +4,7 @@ import re
4
4
  from django.core import exceptions
5
5
  from django.core.validators import MaxLengthValidator, RegexValidator
6
6
  from django.db import models
7
+ from django.forms import TextInput
7
8
  from django.utils.text import slugify
8
9
  from django_extensions.db.fields import AutoSlugField as _AutoSlugField
9
10
  from netaddr import AddrFormatError, EUI, mac_unix_expanded
@@ -11,6 +12,7 @@ from taggit.managers import TaggableManager
11
12
 
12
13
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
13
14
  from nautobot.core.forms import fields, widgets
15
+ from nautobot.core.forms.utils import compress_range, parse_numeric_range
14
16
  from nautobot.core.models import ordering
15
17
  from nautobot.core.models.managers import TagsManager
16
18
  from nautobot.core.models.validators import EnhancedURLValidator
@@ -415,3 +417,57 @@ class TagsField(TaggableManager):
415
417
  kwargs.setdefault("required", False)
416
418
  kwargs.setdefault("query_params", {"content_types": self.model._meta.label_lower})
417
419
  return super().formfield(form_class=form_class, **kwargs)
420
+
421
+
422
+ class PositiveRangeNumberTextField(models.TextField):
423
+ default_error_messages = {
424
+ "invalid": "Invalid value. Specify a value using non-negative integers in a range format (i.e. '10-20').",
425
+ }
426
+
427
+ description = "A text based representation of positive number range."
428
+
429
+ def __init__(self, min_boundary=0, max_boundary=None, *args, **kwargs):
430
+ super().__init__(*args, **kwargs)
431
+ self.min_boundary = min_boundary
432
+ self.max_boundary = max_boundary
433
+
434
+ def to_python(self, value):
435
+ if value is None:
436
+ return None
437
+
438
+ try:
439
+ self.expanded = sorted(parse_numeric_range(value))
440
+ except (ValueError, AttributeError):
441
+ raise exceptions.ValidationError(
442
+ self.error_messages["invalid"],
443
+ code="invalid",
444
+ params={"value": value},
445
+ )
446
+
447
+ converted_ranges = compress_range(self.expanded)
448
+ normalized_range = ",".join([f"{x[0]}" if x[0] == x[1] else f"{x[0]}-{x[1]}" for x in converted_ranges])
449
+
450
+ return normalized_range
451
+
452
+ def validate(self, value, model_instance):
453
+ """
454
+ Validate `value` and raise ValidationError if necessary.
455
+ """
456
+ super().validate(value, model_instance)
457
+
458
+ if (self.min_boundary is not None and self.expanded[0] < self.min_boundary) or (
459
+ self.max_boundary is not None and self.expanded[-1] > self.max_boundary
460
+ ):
461
+ raise exceptions.ValidationError(
462
+ message=f"Invalid value. Specify a range value between {self.min_boundary}-{self.max_boundary or 'unlimited'}",
463
+ code="outofrange",
464
+ params={"value": value},
465
+ )
466
+
467
+ def formfield(self, **kwargs):
468
+ return super().formfield(
469
+ **{
470
+ "widget": TextInput,
471
+ **kwargs,
472
+ }
473
+ )
@@ -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
  #