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
@@ -284,9 +284,7 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
284
284
  "data": post_data(test_form_data),
285
285
  }
286
286
  response = self.client.post(**request)
287
- self.assertHttpStatus(response, 200)
288
- response_body = response.content.decode(response.charset)
289
- self.assertIn("“Generic Site” is not a valid UUID.", response_body)
287
+ self.assertBodyContains(response, "“Generic Site” is not a valid UUID.")
290
288
  test_form_data["parent"] = site_1.pk
291
289
  request["data"] = post_data(test_form_data)
292
290
  self.assertHttpStatus(self.client.post(**request), 302)
@@ -1172,8 +1170,7 @@ module-bays:
1172
1170
  }
1173
1171
 
1174
1172
  response = self.client.post(url, data)
1175
- self.assertHttpStatus(response, 200)
1176
- self.assertIn("failed validation", response.content.decode(response.charset))
1173
+ self.assertBodyContains(response, "failed validation")
1177
1174
 
1178
1175
 
1179
1176
  class ModuleTypeTestCase(
@@ -2279,12 +2276,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2279
2276
 
2280
2277
  url = reverse("dcim:device_interfaces", kwargs={"pk": device.pk})
2281
2278
  response = self.client.get(url)
2282
- self.assertHttpStatus(response, 200)
2283
- response_body = response.content.decode(response.charset)
2284
- # Count the number of occurrences of "Add IP address" in the response_body
2285
- count = response_body.count("Add IP address")
2286
2279
  # Assert that "Add IP address" appears for each of the three interfaces
2287
- self.assertEqual(count, 3)
2280
+ self.assertBodyContains(response, "Add IP address", count=3)
2288
2281
 
2289
2282
  def test_device_interface_assign_ipaddress(self):
2290
2283
  device = Device.objects.first()
@@ -2322,29 +2315,19 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2322
2315
  "data": post_data(assign_ip_form_data),
2323
2316
  }
2324
2317
 
2325
- with self.subTest("Assert Cannnot assign IPAddress('Add New') without permission"):
2318
+ with self.subTest("Assert Cannot assign IPAddress('Add New') without permission"):
2326
2319
  # Assert Add new IPAddress
2327
2320
  response = self.client.post(**add_new_ip_request, follow=True)
2328
- response_body = response.content.decode(response.charset)
2329
- self.assertHttpStatus(response, 200)
2321
+ self.assertBodyContains(response, f"Interface with id "{self.interfaces[0].pk}" not found")
2330
2322
  self.interfaces[0].refresh_from_db()
2331
2323
  self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
2332
- self.assertIn(
2333
- f"Interface with id "{self.interfaces[0].pk}" not found",
2334
- response_body,
2335
- )
2336
2324
 
2337
- with self.subTest("Assert Cannnot assign IPAddress(Exsisting IP) without permission"):
2325
+ with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
2338
2326
  # Assert Assign Exsisting IPAddress
2339
2327
  response = self.client.post(**assign_ip_request, follow=True)
2340
- response_body = response.content.decode(response.charset)
2341
- self.assertHttpStatus(response, 200)
2328
+ self.assertBodyContains(response, f"Interface with id "{self.interfaces[1].pk}" not found")
2342
2329
  self.interfaces[1].refresh_from_db()
2343
2330
  self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
2344
- self.assertIn(
2345
- f"Interface with id "{self.interfaces[1].pk}" not found",
2346
- response_body,
2347
- )
2348
2331
 
2349
2332
  self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
2350
2333
 
@@ -2375,10 +2358,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2375
2358
  "data": post_data(assign_ip_form_data),
2376
2359
  }
2377
2360
  response = self.client.post(**assign_ip_request, follow=True)
2378
- self.assertHttpStatus(response, 200)
2379
- self.assertIn(
2380
- "Please select at least one IP Address from the table.", response.content.decode(response.charset)
2381
- )
2361
+ self.assertBodyContains(response, "Please select at least one IP Address from the table.")
2382
2362
 
2383
2363
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
2384
2364
  def test_device_rearports(self):
@@ -2683,12 +2663,8 @@ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2683
2663
 
2684
2664
  url = reverse("dcim:module_interfaces", kwargs={"pk": module.pk})
2685
2665
  response = self.client.get(url)
2686
- self.assertHttpStatus(response, 200)
2687
- response_body = response.content.decode(response.charset)
2688
- # Count the number of occurrences of "Add IP address" in the response_body
2689
- count = response_body.count("Add IP address")
2690
2666
  # Assert that "Add IP address" appears for each of the three interfaces
2691
- self.assertEqual(count, 3)
2667
+ self.assertBodyContains(response, "Add IP address", count=3)
2692
2668
 
2693
2669
  def test_module_interface_assign_ipaddress(self):
2694
2670
  module = Module.objects.first()
@@ -2726,29 +2702,19 @@ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
2726
2702
  "data": post_data(assign_ip_form_data),
2727
2703
  }
2728
2704
 
2729
- with self.subTest("Assert Cannnot assign IPAddress('Add New') without permission"):
2705
+ with self.subTest("Assert Cannot assign IPAddress('Add New') without permission"):
2730
2706
  # Assert Add new IPAddress
2731
2707
  response = self.client.post(**add_new_ip_request, follow=True)
2732
- response_body = response.content.decode(response.charset)
2733
- self.assertHttpStatus(response, 200)
2708
+ self.assertBodyContains(response, f"Interface with id "{self.interfaces[0].pk}" not found")
2734
2709
  self.interfaces[0].refresh_from_db()
2735
2710
  self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
2736
- self.assertIn(
2737
- f"Interface with id "{self.interfaces[0].pk}" not found",
2738
- response_body,
2739
- )
2740
2711
 
2741
- with self.subTest("Assert Cannnot assign IPAddress(Exsisting IP) without permission"):
2712
+ with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
2742
2713
  # Assert Assign Exsisting IPAddress
2743
2714
  response = self.client.post(**assign_ip_request, follow=True)
2744
- response_body = response.content.decode(response.charset)
2745
- self.assertHttpStatus(response, 200)
2715
+ self.assertBodyContains(response, f"Interface with id "{self.interfaces[1].pk}" not found")
2746
2716
  self.interfaces[1].refresh_from_db()
2747
2717
  self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
2748
- self.assertIn(
2749
- f"Interface with id "{self.interfaces[1].pk}" not found",
2750
- response_body,
2751
- )
2752
2718
 
2753
2719
  self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
2754
2720
 
@@ -3228,8 +3194,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
3228
3194
  invalid_ipaddress_link = reverse("ipam:ipaddress_edit", args=(ipaddress.pk,))
3229
3195
  valid_ipaddress_link = ipaddress.get_absolute_url()
3230
3196
  response = self.client.get(interface.get_absolute_url() + "?tab=main")
3231
- response_content = response.content.decode(response.charset)
3232
- self.assertIn(valid_ipaddress_link, response_content)
3197
+ self.assertBodyContains(response, valid_ipaddress_link)
3198
+ response_content = extract_page_body(response.content.decode(response.charset))
3233
3199
  self.assertNotIn(invalid_ipaddress_link, response_content)
3234
3200
 
3235
3201
 
@@ -4113,7 +4079,6 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
4113
4079
  response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
4114
4080
  self.assertHttpStatus(response, 200)
4115
4081
  content = extract_page_body(response.content.decode(response.charset))
4116
- # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
4117
4082
  if hasattr(self.model, "name"):
4118
4083
  self.assertIn(instance1.name, content, msg=content)
4119
4084
  self.assertNotIn(instance2.name, content, msg=content)
@@ -4209,11 +4174,11 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4209
4174
  Interface.objects.create(device=self.devices[2], name="device 2 interface 1", status=interface_status)
4210
4175
  Interface.objects.create(device=self.devices[2], name="device 2 interface 2", status=interface_status)
4211
4176
  response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
4212
- self.assertIn('Interfaces <span class="badge">6</span>', str(response.content))
4213
- self.assertIn("device 1 interface 1", str(response.content))
4214
- self.assertIn("device 1 interface 2", str(response.content))
4215
- self.assertIn("device 2 interface 1", str(response.content))
4216
- self.assertIn("device 2 interface 2", str(response.content))
4177
+ self.assertBodyContains(response, 'Interfaces <span class="badge">6</span>')
4178
+ self.assertBodyContains(response, "device 1 interface 1")
4179
+ self.assertBodyContains(response, "device 1 interface 2")
4180
+ self.assertBodyContains(response, "device 2 interface 1")
4181
+ self.assertBodyContains(response, "device 2 interface 2")
4217
4182
 
4218
4183
  def test_device_column_visible(self):
4219
4184
  """
@@ -4226,7 +4191,7 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4226
4191
  Interface.objects.create(device=self.devices[0], name="eth0", status=interface_status)
4227
4192
  Interface.objects.create(device=self.devices[0], name="eth1", status=interface_status)
4228
4193
  response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
4229
- self.assertIn("<th >Device</th>", str(response.content))
4194
+ self.assertBodyContains(response, "<th>Device</th>", html=True)
4230
4195
 
4231
4196
  def test_device_column_not_visible(self):
4232
4197
  """
@@ -4239,9 +4204,9 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4239
4204
  Interface.objects.create(device=self.devices[1], name="eth2", status=interface_status)
4240
4205
  Interface.objects.create(device=self.devices[1], name="eth3", status=interface_status)
4241
4206
  response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[1].pk}))
4242
- self.assertNotIn("<th >Device</th>", str(response.content))
4207
+ self.assertNotIn("<th >Device</th>", extract_page_body(response.content.decode(response.charset)))
4243
4208
  # Sanity check:
4244
- self.assertIn("<th >Name</th>", str(response.content))
4209
+ self.assertBodyContains(response, "<th>Name</th>", html=True)
4245
4210
 
4246
4211
 
4247
4212
  class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -4408,9 +4373,7 @@ class PathTraceViewTestCase(ModelViewTestCase):
4408
4373
  url = reverse("dcim:rearport_trace", args=[obj.pk])
4409
4374
  cablepath_id = CablePath.objects.first().id
4410
4375
  response = self.client.get(url + f"?cablepath_id={cablepath_id}")
4411
- self.assertHttpStatus(response, 200)
4412
- content = extract_page_body(response.content.decode(response.charset))
4413
- self.assertInHTML("<h1>Cable Trace for Rear Port Rear Port 1</h1>", content)
4376
+ self.assertBodyContains(response, "<h1>Cable Trace for Rear Port Rear Port 1</h1>", html=True)
4414
4377
 
4415
4378
 
4416
4379
  class DeviceRedundancyGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -4621,10 +4584,8 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4621
4584
  "_confirm": True, # Form button
4622
4585
  }
4623
4586
  response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
4624
- self.assertHttpStatus(response, 200)
4625
- response_body = response.content.decode(response.charset)
4626
4587
  # Assert protected error message included in the response body
4627
- self.assertInHTML(f"<span>{device_type_to_software_image_file}</span>", response_body)
4588
+ self.assertBodyContains(response, f"<span>{device_type_to_software_image_file}</span>", html=True)
4628
4589
 
4629
4590
 
4630
4591
  class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -4701,10 +4662,8 @@ class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
4701
4662
  "_confirm": True, # Form button
4702
4663
  }
4703
4664
  response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
4704
- self.assertHttpStatus(response, 200)
4705
- response_body = response.content.decode(response.charset)
4706
4665
  # Assert protected error message included in the response body
4707
- self.assertInHTML(f"<span>{device_type_to_software_image_file}</span>", response_body)
4666
+ self.assertBodyContains(response, f"<span>{device_type_to_software_image_file}</span>", html=True)
4708
4667
 
4709
4668
 
4710
4669
  class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -177,7 +177,12 @@ def ensure_git_repository(repository_record, logger=None, head=None): # pylint:
177
177
  if logger:
178
178
  if changed:
179
179
  logger.info("Repository successfully refreshed")
180
- logger.info(f'The current Git repository hash is "{repository_record.current_head}"')
180
+ logger.info(
181
+ '%s: the current Git repository hash is "%s"',
182
+ repository_record.name,
183
+ repository_record.current_head,
184
+ extra={"object": repository_record},
185
+ )
181
186
 
182
187
  return changed
183
188
 
@@ -10,6 +10,9 @@ def set_dynamic_group_group_types(apps, schema_editor):
10
10
  # The group_type field defaults to TYPE_DYNAMIC_FILTER
11
11
  # There are no preexisting TYPE_STATIC groups as that's a new feature
12
12
  # Any group that has children should be converted to TYPE_DYNAMIC_SET
13
+ # NOTE: The below is actually incorrect (see migration 0116) as for some reason, during migrations ONLY,
14
+ # Django somehow swaps the `parent` and `children` relations on DynamicGroup such that the below actually detects
15
+ # the opposite set of groups from what would be expected.
13
16
  DynamicGroup.objects.filter(children__isnull=False).distinct().update(
14
17
  group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
15
18
  )
@@ -0,0 +1,16 @@
1
+ from django.db import migrations
2
+
3
+ from nautobot.extras.utils import fixup_dynamic_group_group_types
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("extras", "0115_scheduledjob_time_zone"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.RunPython(
13
+ code=fixup_dynamic_group_group_types,
14
+ reverse_code=migrations.operations.special.RunPython.noop,
15
+ ),
16
+ ]
@@ -15,7 +15,7 @@ from nautobot.core.models.fields import slugify_dashes_to_underscores
15
15
  from nautobot.core.tables import CustomFieldColumn
16
16
  from nautobot.core.testing import APITestCase, TestCase, TransactionTestCase
17
17
  from nautobot.core.testing.models import ModelTestCases
18
- from nautobot.core.testing.utils import post_data
18
+ from nautobot.core.testing.utils import extract_page_body, post_data
19
19
  from nautobot.core.utils.lookup import get_changes_for_model
20
20
  from nautobot.dcim.filters import LocationFilterSet
21
21
  from nautobot.dcim.forms import RackFilterForm
@@ -995,18 +995,15 @@ class CustomFieldDataAPITest(APITestCase):
995
995
  },
996
996
  }
997
997
  response = self.client.post(self.list_url, data, format="json", **self.header)
998
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
999
- self.assertIn("Value must be a string", str(response.content))
998
+ self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
1000
999
 
1001
1000
  data["custom_fields"].update({self.cf_text.key: 2})
1002
1001
  response = self.client.post(self.list_url, data, format="json", **self.header)
1003
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1004
- self.assertIn("Value must be a string", str(response.content))
1002
+ self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
1005
1003
 
1006
1004
  data["custom_fields"].update({self.cf_text.key: True})
1007
1005
  response = self.client.post(self.list_url, data, format="json", **self.header)
1008
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1009
- self.assertIn("Value must be a string", str(response.content))
1006
+ self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
1010
1007
 
1011
1008
  def test_create_without_required_field(self):
1012
1009
  self.cf_text.default = None
@@ -1019,8 +1016,7 @@ class CustomFieldDataAPITest(APITestCase):
1019
1016
  "status": self.statuses[0].pk,
1020
1017
  }
1021
1018
  response = self.client.post(self.list_url, data, format="json", **self.header)
1022
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1023
- self.assertIn("Required field cannot be empty", str(response.content))
1019
+ self.assertContains(response, "Required field cannot be empty", status_code=status.HTTP_400_BAD_REQUEST)
1024
1020
 
1025
1021
  # Try in CSV format too
1026
1022
  csvdata = "\n".join(
@@ -1030,8 +1026,7 @@ class CustomFieldDataAPITest(APITestCase):
1030
1026
  ]
1031
1027
  )
1032
1028
  response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
1033
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1034
- self.assertIn("Required field cannot be empty", str(response.content))
1029
+ self.assertContains(response, "Required field cannot be empty", status_code=status.HTTP_400_BAD_REQUEST)
1035
1030
 
1036
1031
  def test_create_invalid_select_choice(self):
1037
1032
  data = {
@@ -1043,8 +1038,7 @@ class CustomFieldDataAPITest(APITestCase):
1043
1038
  },
1044
1039
  }
1045
1040
  response = self.client.post(self.list_url, data, format="json", **self.header)
1046
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1047
- self.assertIn("Invalid choice", str(response.content))
1041
+ self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
1048
1042
 
1049
1043
  # Try in CSV format too
1050
1044
  csvdata = "\n".join(
@@ -1054,8 +1048,7 @@ class CustomFieldDataAPITest(APITestCase):
1054
1048
  ]
1055
1049
  )
1056
1050
  response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
1057
- self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
1058
- self.assertIn("Invalid choice", str(response.content))
1051
+ self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
1059
1052
 
1060
1053
 
1061
1054
  class CustomFieldImportTest(TestCase):
@@ -1157,7 +1150,7 @@ class CustomFieldImportTest(TestCase):
1157
1150
  try:
1158
1151
  location1 = Location.objects.get(name="Location 1")
1159
1152
  except Location.DoesNotExist:
1160
- self.fail(str(response.content))
1153
+ self.fail(extract_page_body(response.content.decode(response.charset)))
1161
1154
  self.assertEqual(len(location1.cf), 8)
1162
1155
  self.assertEqual(location1.cf["text"], "ABC")
1163
1156
  self.assertEqual(location1.cf["integer"], 123)
@@ -1,5 +1,6 @@
1
1
  import random
2
2
 
3
+ from django.apps import apps
3
4
  from django.contrib.contenttypes.models import ContentType
4
5
  from django.core.exceptions import ValidationError
5
6
  from django.db.models import ProtectedError, QuerySet
@@ -40,6 +41,7 @@ from nautobot.extras.models import (
40
41
  Status,
41
42
  Tag,
42
43
  )
44
+ from nautobot.extras.utils import fixup_dynamic_group_group_types
43
45
  from nautobot.ipam.models import IPAddress, Prefix
44
46
  from nautobot.ipam.querysets import PrefixQuerySet
45
47
  from nautobot.tenancy.models import Tenant
@@ -1224,3 +1226,117 @@ class DynamicGroupMembershipFilterTest(DynamicGroupTestBase, FilterTestCases.Fil
1224
1226
  for value, cnt in tests.items():
1225
1227
  params = {"q": value}
1226
1228
  self.assertEqual(self.filterset(params, self.queryset).qs.count(), cnt)
1229
+
1230
+
1231
+ class DynamicGroupFixupTestCase(TestCase):
1232
+ """Check for the correct functioning of the fixup_dynamic_group_group_types() data migration helper function."""
1233
+
1234
+ def test_fixup_dynamic_group_group_types(self):
1235
+ device_ct = ContentType.objects.get_for_model(Device)
1236
+
1237
+ good_grandparent_group = DynamicGroup.objects.create(
1238
+ name="Good Grandparent",
1239
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
1240
+ content_type=device_ct,
1241
+ )
1242
+ bad_grandparent_group = DynamicGroup.objects.create(
1243
+ name="Bad Grandparent",
1244
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER, # wrong, but possible due to #6329
1245
+ content_type=device_ct,
1246
+ )
1247
+ good_parent_group = DynamicGroup.objects.create(
1248
+ name="Good Parent",
1249
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
1250
+ content_type=device_ct,
1251
+ )
1252
+ bad_parent_group = DynamicGroup.objects.create(
1253
+ name="Bad Parent",
1254
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER, # wrong, #6329 again
1255
+ content_type=device_ct,
1256
+ )
1257
+ good_child_group = DynamicGroup.objects.create(
1258
+ name="Good Child",
1259
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
1260
+ content_type=device_ct,
1261
+ filter={"status": [Status.objects.get_for_model(Device).first().name]},
1262
+ )
1263
+ bad_child_group = DynamicGroup.objects.create(
1264
+ name="Bad Child",
1265
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET, # wrong, #6329 again
1266
+ content_type=device_ct,
1267
+ filter={"status": [Status.objects.get_for_model(Device).first().name]},
1268
+ )
1269
+
1270
+ DynamicGroupMembership.objects.create(
1271
+ parent_group=good_grandparent_group,
1272
+ group=good_parent_group,
1273
+ weight=10,
1274
+ operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
1275
+ )
1276
+ DynamicGroupMembership.objects.create(
1277
+ parent_group=bad_grandparent_group,
1278
+ group=bad_parent_group,
1279
+ weight=10,
1280
+ operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
1281
+ )
1282
+ DynamicGroupMembership.objects.create(
1283
+ parent_group=good_parent_group,
1284
+ group=good_child_group,
1285
+ weight=10,
1286
+ operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
1287
+ )
1288
+ DynamicGroupMembership.objects.create(
1289
+ parent_group=bad_parent_group,
1290
+ group=bad_child_group,
1291
+ weight=10,
1292
+ operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
1293
+ )
1294
+
1295
+ good_standalone_group_1 = DynamicGroup.objects.create(
1296
+ name="Good Standalone Group 1",
1297
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER,
1298
+ content_type=device_ct,
1299
+ # empty filter - this is OK!
1300
+ )
1301
+ good_standalone_group_2 = DynamicGroup.objects.create(
1302
+ name="Good Standalone Group 2",
1303
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
1304
+ content_type=device_ct,
1305
+ )
1306
+ bad_standalone_group = DynamicGroup.objects.create(
1307
+ name="Bad Standalone Group",
1308
+ group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET,
1309
+ content_type=device_ct,
1310
+ filter={"status": [Status.objects.get_for_model(Device).first().name]},
1311
+ )
1312
+
1313
+ # DynamicGroupMembership.save() will actually auto-fixup the type on bad_parent_group and bad_grandparent_group.
1314
+ # Make them wrong again:
1315
+ bad_grandparent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
1316
+ bad_grandparent_group.save()
1317
+ bad_parent_group.group_type = DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER
1318
+ bad_parent_group.save()
1319
+
1320
+ count_1, count_2 = fixup_dynamic_group_group_types(apps)
1321
+
1322
+ self.assertEqual(count_1, 2) # bad_grandparent_group, bad_parent_group
1323
+ self.assertEqual(count_2, 2) # bad_child_group, bad_standalone_group
1324
+
1325
+ good_grandparent_group.refresh_from_db()
1326
+ self.assertEqual(good_grandparent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # unchanged
1327
+ bad_grandparent_group.refresh_from_db()
1328
+ self.assertEqual(bad_grandparent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # fixed
1329
+ good_parent_group.refresh_from_db()
1330
+ self.assertEqual(good_parent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # unchanged
1331
+ bad_parent_group.refresh_from_db()
1332
+ self.assertEqual(bad_parent_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # fixed
1333
+ good_child_group.refresh_from_db()
1334
+ self.assertEqual(good_child_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # unchanged
1335
+ bad_child_group.refresh_from_db()
1336
+ self.assertEqual(bad_child_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # fixed
1337
+ good_standalone_group_1.refresh_from_db()
1338
+ self.assertEqual(good_standalone_group_1.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # unchanged
1339
+ good_standalone_group_2.refresh_from_db()
1340
+ self.assertEqual(good_standalone_group_2.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_SET) # unchanged
1341
+ bad_standalone_group.refresh_from_db()
1342
+ self.assertEqual(bad_standalone_group.group_type, DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # fixed
@@ -694,17 +694,15 @@ class TestAppCoreViewOverrides(TestCase):
694
694
 
695
695
  def test_views_are_overridden(self):
696
696
  response = self.client.get(reverse("plugins:example_app:view_to_be_overridden"))
697
- self.assertEqual(b"Hello world! I'm an overridden view.", response.content)
697
+ self.assertEqual("Hello world! I'm an overridden view.", response.content.decode(response.charset))
698
698
 
699
699
  response = self.client.get(
700
700
  f'{reverse("plugins:plugin_detail", kwargs={"plugin": "example_app_with_view_override"})}'
701
701
  )
702
702
  self.assertIn(
703
- (
704
- b"plugins:example_app:view_to_be_overridden <code>"
705
- b"example_app_with_view_override.views.ViewOverride</code>"
706
- ),
707
- response.content,
703
+ "plugins:example_app:view_to_be_overridden <code>"
704
+ "example_app_with_view_override.views.ViewOverride</code>",
705
+ extract_page_body(response.content.decode(response.charset)),
708
706
  )
709
707
 
710
708
 
@@ -1,5 +1,7 @@
1
1
  from unittest import mock
2
2
 
3
+ from django.core.cache import cache
4
+
3
5
  from nautobot.core.testing import TestCase
4
6
  from nautobot.extras.registry import registry
5
7
  from nautobot.extras.utils import get_celery_queues, get_worker_count, populate_model_features_registry
@@ -17,6 +19,7 @@ class UtilsTestCase(TestCase):
17
19
  self.assertDictEqual(get_celery_queues(), {"queue1": 1})
18
20
 
19
21
  with self.subTest("2 workers 2 shared queues"):
22
+ cache.clear()
20
23
  mock_active_queues.return_value = {
21
24
  "celery@worker1": [{"name": "queue1"}, {"name": "queue2"}],
22
25
  "celery@worker2": [{"name": "queue1"}, {"name": "queue2"}],
@@ -24,6 +27,7 @@ class UtilsTestCase(TestCase):
24
27
  self.assertDictEqual(get_celery_queues(), {"queue1": 2, "queue2": 2})
25
28
 
26
29
  with self.subTest("2 workers 2 individual queues"):
30
+ cache.clear()
27
31
  mock_active_queues.return_value = {
28
32
  "celery@worker1": [{"name": "queue1"}],
29
33
  "celery@worker2": [{"name": "queue2"}],
@@ -31,6 +35,7 @@ class UtilsTestCase(TestCase):
31
35
  self.assertDictEqual(get_celery_queues(), {"queue1": 1, "queue2": 1})
32
36
 
33
37
  with self.subTest("2 workers 3 queues"):
38
+ cache.clear()
34
39
  mock_active_queues.return_value = {
35
40
  "celery@worker1": [{"name": "queue1"}, {"name": "queue3"}],
36
41
  "celery@worker2": [{"name": "queue2"}],