nautobot 2.2.6__py3-none-any.whl → 2.2.8__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 (292) hide show
  1. nautobot/core/api/mixins.py +6 -1
  2. nautobot/core/api/schema.py +3 -1
  3. nautobot/core/celery/__init__.py +1 -1
  4. nautobot/core/graphql/generators.py +2 -2
  5. nautobot/core/graphql/schema.py +7 -4
  6. nautobot/core/tests/integration/test_general_functionality.py +36 -0
  7. nautobot/core/tests/runner.py +10 -0
  8. nautobot/core/tests/test_graphql.py +51 -5
  9. nautobot/dcim/models/devices.py +10 -3
  10. nautobot/dcim/tests/test_models.py +75 -0
  11. nautobot/extras/models/groups.py +9 -2
  12. nautobot/extras/models/relationships.py +12 -0
  13. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  14. nautobot/extras/tests/test_dynamicgroups.py +8 -1
  15. nautobot/extras/tests/test_relationships.py +221 -1
  16. nautobot/extras/views.py +1 -1
  17. nautobot/ipam/api/serializers.py +46 -16
  18. nautobot/ipam/api/views.py +29 -16
  19. nautobot/ipam/tests/test_api.py +15 -20
  20. nautobot/project-static/docs/404.html +3 -3
  21. nautobot/project-static/docs/apps/index.html +3 -3
  22. nautobot/project-static/docs/apps/nautobot-apps.html +3 -3
  23. nautobot/project-static/docs/assets/javascripts/{bundle.ad660dcc.min.js → bundle.fe8b6f2b.min.js} +4 -4
  24. nautobot/project-static/docs/assets/javascripts/{bundle.ad660dcc.min.js.map → bundle.fe8b6f2b.min.js.map} +3 -3
  25. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css → main.76a95c52.min.css} +1 -1
  26. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css.map → main.76a95c52.min.css.map} +1 -1
  27. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +3 -3
  28. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +3 -3
  29. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3 -3
  30. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +3 -3
  31. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +3 -3
  32. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +3 -3
  33. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +3 -3
  34. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +3 -3
  35. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +3 -3
  36. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +3 -3
  37. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +3 -3
  38. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +3 -3
  39. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
  40. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
  41. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
  42. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
  43. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
  44. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
  45. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +3 -3
  46. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +3 -3
  47. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +3 -3
  48. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +3 -3
  49. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3 -3
  50. nautobot/project-static/docs/development/apps/api/configuration-view.html +3 -3
  51. nautobot/project-static/docs/development/apps/api/database-backend-config.html +3 -3
  52. nautobot/project-static/docs/development/apps/api/models/django-admin.html +3 -3
  53. nautobot/project-static/docs/development/apps/api/models/global-search.html +3 -3
  54. nautobot/project-static/docs/development/apps/api/models/graphql.html +3 -3
  55. nautobot/project-static/docs/development/apps/api/models/index.html +3 -3
  56. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
  57. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
  58. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +3 -3
  59. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
  60. nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
  61. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
  62. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
  63. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
  64. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
  65. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
  66. nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
  67. nautobot/project-static/docs/development/apps/api/setup.html +3 -3
  68. nautobot/project-static/docs/development/apps/api/testing.html +3 -3
  69. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
  70. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
  71. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
  72. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
  73. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
  74. nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
  75. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
  76. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
  77. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
  78. nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
  79. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
  80. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
  81. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
  82. nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
  83. nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
  84. nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
  85. nautobot/project-static/docs/development/apps/index.html +3 -3
  86. nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
  87. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
  88. nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
  89. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
  90. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
  91. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
  92. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
  93. nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
  94. nautobot/project-static/docs/development/core/application-registry.html +3 -3
  95. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  96. nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
  97. nautobot/project-static/docs/development/core/caching.html +3 -3
  98. nautobot/project-static/docs/development/core/controllers.html +3 -3
  99. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
  100. nautobot/project-static/docs/development/core/generic-views.html +3 -3
  101. nautobot/project-static/docs/development/core/getting-started.html +5 -6
  102. nautobot/project-static/docs/development/core/homepage.html +3 -3
  103. nautobot/project-static/docs/development/core/index.html +3 -3
  104. nautobot/project-static/docs/development/core/model-checklist.html +3 -3
  105. nautobot/project-static/docs/development/core/model-features.html +3 -3
  106. nautobot/project-static/docs/development/core/natural-keys.html +3 -3
  107. nautobot/project-static/docs/development/core/navigation-menu.html +3 -3
  108. nautobot/project-static/docs/development/core/release-checklist.html +3 -3
  109. nautobot/project-static/docs/development/core/role-internals.html +3 -3
  110. nautobot/project-static/docs/development/core/settings.html +3 -3
  111. nautobot/project-static/docs/development/core/style-guide.html +3 -3
  112. nautobot/project-static/docs/development/core/templates.html +3 -3
  113. nautobot/project-static/docs/development/core/testing.html +16 -4
  114. nautobot/project-static/docs/development/core/user-preferences.html +3 -3
  115. nautobot/project-static/docs/development/index.html +3 -3
  116. nautobot/project-static/docs/development/jobs/index.html +3 -3
  117. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -3
  118. nautobot/project-static/docs/objects.inv +0 -0
  119. nautobot/project-static/docs/overview/application_stack.html +3 -3
  120. nautobot/project-static/docs/overview/design_philosophy.html +3 -3
  121. nautobot/project-static/docs/overview/index.html +3 -3
  122. nautobot/project-static/docs/release-notes/index.html +3 -3
  123. nautobot/project-static/docs/release-notes/version-1.0.html +3 -3
  124. nautobot/project-static/docs/release-notes/version-1.1.html +3 -3
  125. nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
  126. nautobot/project-static/docs/release-notes/version-1.3.html +3 -3
  127. nautobot/project-static/docs/release-notes/version-1.4.html +3 -3
  128. nautobot/project-static/docs/release-notes/version-1.5.html +3 -3
  129. nautobot/project-static/docs/release-notes/version-1.6.html +3 -3
  130. nautobot/project-static/docs/release-notes/version-2.0.html +3 -3
  131. nautobot/project-static/docs/release-notes/version-2.1.html +3 -3
  132. nautobot/project-static/docs/release-notes/version-2.2.html +370 -99
  133. nautobot/project-static/docs/requirements.txt +2 -1
  134. nautobot/project-static/docs/search/search_index.json +1 -1
  135. nautobot/project-static/docs/sitemap.xml +256 -256
  136. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  137. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  138. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +3 -3
  139. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +3 -3
  140. nautobot/project-static/docs/user-guide/administration/configuration/index.html +3 -3
  141. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +3 -3
  142. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +3 -3
  143. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +3 -3
  144. nautobot/project-static/docs/user-guide/administration/guides/caching.html +3 -3
  145. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
  146. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +3 -3
  147. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +3 -3
  148. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +3 -3
  149. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +3 -3
  150. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +3 -3
  151. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +3 -3
  152. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +3 -3
  153. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +3 -3
  154. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +7 -7
  155. nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
  156. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +8 -4
  157. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +3 -3
  158. nautobot/project-static/docs/user-guide/administration/installation/services.html +3 -3
  159. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +3 -3
  160. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +3 -3
  161. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +3 -3
  162. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +3 -3
  163. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  164. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +3 -3
  165. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +3 -3
  166. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +3 -3
  167. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +3 -3
  168. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +3 -3
  169. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +3 -3
  170. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +3 -3
  171. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +3 -3
  172. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +3 -3
  173. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +3 -3
  175. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +3 -3
  176. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +3 -3
  177. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +3 -3
  178. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +3 -3
  179. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +3 -3
  180. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +3 -3
  181. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +3 -3
  182. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +3 -3
  183. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +3 -3
  184. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +3 -3
  185. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +3 -3
  186. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +3 -3
  187. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +3 -3
  188. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +3 -3
  189. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +3 -3
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +3 -3
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +3 -3
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +3 -3
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +3 -3
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +3 -3
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +3 -3
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +3 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +3 -3
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +3 -3
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +3 -3
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +3 -3
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +3 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +3 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +3 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +3 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +3 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +3 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +3 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +3 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +3 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +3 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +3 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +3 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +3 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +3 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +3 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +3 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +3 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +3 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +3 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +3 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +3 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +3 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +3 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +3 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +3 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +3 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +3 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +3 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
  238. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
  239. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  240. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +3 -3
  241. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
  242. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
  243. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +3 -3
  244. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
  245. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
  246. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
  247. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
  248. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +3 -3
  249. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
  250. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
  251. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
  252. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
  253. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
  254. nautobot/project-static/docs/user-guide/index.html +3 -3
  255. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
  256. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
  257. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
  258. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
  259. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
  260. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
  261. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
  262. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
  263. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
  264. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
  265. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
  266. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
  267. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +3 -3
  268. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
  269. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
  270. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +3 -3
  271. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +3 -3
  272. nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
  273. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +3 -3
  274. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +3 -3
  275. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +3 -3
  276. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +3 -3
  277. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +3 -3
  278. nautobot/project-static/docs/user-guide/platform-functionality/role.html +3 -3
  279. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
  280. nautobot/project-static/docs/user-guide/platform-functionality/status.html +3 -3
  281. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +3 -3
  282. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +3 -3
  283. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +3 -3
  284. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +3 -3
  285. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +3 -3
  286. nautobot/project-static/js/connection_toggles.js +7 -6
  287. {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/METADATA +4 -4
  288. {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/RECORD +292 -291
  289. {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/LICENSE.txt +0 -0
  290. {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/NOTICE +0 -0
  291. {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/WHEEL +0 -0
  292. {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,7 @@ from django.core.exceptions import (
6
6
  MultipleObjectsReturned,
7
7
  ObjectDoesNotExist,
8
8
  )
9
- from django.db.models import AutoField
9
+ from django.db.models import AutoField, Model
10
10
  from rest_framework.exceptions import ValidationError
11
11
 
12
12
  from nautobot.core.api.utils import dict_to_filter_params
@@ -70,6 +70,11 @@ class WritableSerializerMixin:
70
70
  # Strip the trailing slash and split on slashes, taking the last value as the PK.
71
71
  data = data.rstrip("/").split("/")[-1]
72
72
 
73
+ # If we're passing the validated_data from one serializer as input to another serializer,
74
+ # data might already be a model instance:
75
+ if isinstance(data, Model):
76
+ return {"pk": data.pk}
77
+
73
78
  try:
74
79
  # The int case here is taking into account for the User model we inherit from django
75
80
  pk = int(data) if isinstance(queryset.model._meta.pk, AutoField) else uuid.UUID(str(data))
@@ -4,7 +4,7 @@ import re
4
4
  from drf_spectacular.contrib.django_filters import DjangoFilterExtension
5
5
  from drf_spectacular.extensions import OpenApiSerializerFieldExtension
6
6
  from drf_spectacular.openapi import AutoSchema
7
- from drf_spectacular.plumbing import build_array_type, build_media_type_object, get_doc, is_serializer
7
+ from drf_spectacular.plumbing import build_array_type, build_media_type_object, get_doc, is_serializer, list_hash
8
8
  from drf_spectacular.serializers import PolymorphicProxySerializerExtension
9
9
  from rest_framework import serializers
10
10
  from rest_framework.relations import ManyRelatedField
@@ -359,6 +359,8 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
359
359
  return {
360
360
  "type": value_type,
361
361
  "enum": list(choices.keys()),
362
+ # Used to deduplicate enums with the same set of choices, since drf-spectacular 0.27.2
363
+ "x-spec-enum-id": list_hash([(k, v) for k, v in choices.items() if k not in ("", None)]),
362
364
  }
363
365
  else:
364
366
  return {
@@ -109,7 +109,7 @@ def _import_jobs_from_git_repositories():
109
109
  and not GitRepository.objects.filter(slug=filename).exists()
110
110
  ):
111
111
  logger.warning("Deleting unmanaged (leftover?) Git repository clone at %s", filepath)
112
- shutil.rmtree(filepath)
112
+ shutil.rmtree(filepath, ignore_errors=True)
113
113
 
114
114
  # Make sure all GitRepository records that include Jobs have up-to-date git clones, and load their jobs
115
115
  for repo in GitRepository.objects.filter(provided_contents__contains="extras.job"):
@@ -55,12 +55,12 @@ def generate_null_choices_resolver(name, resolver_name):
55
55
 
56
56
  def generate_filter_resolver(schema_type, resolver_name, field_name):
57
57
  """
58
- Generate function to resolve OneToMany filtering.
58
+ Generate function to resolve filtering of ManyToOne and ManyToMany related objects.
59
59
 
60
60
  Args:
61
61
  schema_type (DjangoObjectType): DjangoObjectType for a given model
62
62
  resolver_name (str): name of the resolver
63
- field_name (str): name of OneToMany field to filter
63
+ field_name (str): name of ManyToOneField, ManyToManyRel, or ManyToOneRel field to filter
64
64
  """
65
65
  filterset_class = schema_type._meta.filterset_class
66
66
 
@@ -6,7 +6,8 @@ import logging
6
6
  from django.conf import settings
7
7
  from django.contrib.contenttypes.models import ContentType
8
8
  from django.core.validators import ValidationError
9
- from django.db.models.fields.reverse_related import ManyToOneRel, OneToOneRel
9
+ from django.db.models import ManyToManyField
10
+ from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
10
11
  import graphene
11
12
  from graphene.types import generic
12
13
 
@@ -198,9 +199,11 @@ def extend_schema_type_filter(schema_type, model):
198
199
  (DjangoObjectType): The extended schema_type object
199
200
  """
200
201
  for field in model._meta.get_fields():
201
- # Check attribute is a ManyToOne field
202
- # OneToOneRel is a subclass of ManyToOneRel, but we don't want to treat is as a list
203
- if not isinstance(field, ManyToOneRel) or isinstance(field, OneToOneRel):
202
+ # Check whether attribute is a ManyToOne or ManyToMany field
203
+ if not isinstance(field, (ManyToManyField, ManyToManyRel, ManyToOneRel)):
204
+ continue
205
+ # OneToOneRel is a subclass of ManyToOneRel, but we don't want to treat it as a list
206
+ if isinstance(field, OneToOneRel):
204
207
  continue
205
208
  child_schema_type = registry["graphql_types"].get(field.related_model._meta.label_lower)
206
209
  if child_schema_type:
@@ -0,0 +1,36 @@
1
+ from django.urls import reverse
2
+
3
+ from nautobot.core.testing.integration import SeleniumTestCase
4
+
5
+
6
+ class StaticMediaFailureTestCase(SeleniumTestCase):
7
+ """Integration test to make sure no static media failures are encountered."""
8
+
9
+ def setUp(self):
10
+ super().setUp()
11
+ self.user.is_superuser = True
12
+ self.user.is_staff = True
13
+ self.user.save()
14
+ self.login(self.user.username, self.password)
15
+
16
+ def tearDown(self):
17
+ self.logout()
18
+ super().tearDown()
19
+
20
+ def test_for_static_media_failure(self):
21
+ test_urls = [
22
+ reverse("home"),
23
+ reverse("api-root"),
24
+ reverse("graphql"),
25
+ reverse("api_docs"),
26
+ "/admin/",
27
+ "/static/docs/overview/index.html",
28
+ ]
29
+ for url in test_urls:
30
+ with self.subTest(test_url=url):
31
+ self.browser.visit(self.live_server_url + url)
32
+ # Wait for body element to appear
33
+ self.assertTrue(self.browser.is_element_present_by_tag("body", wait_time=10), "Page failed to load")
34
+ # Ensure we weren't redirected to another page
35
+ self.assertEqual(self.browser.url, self.live_server_url + url)
36
+ self.assertTrue(self.browser.is_text_not_present("Static Media Failure"))
@@ -1,8 +1,10 @@
1
1
  import copy
2
+ import hashlib
2
3
 
3
4
  from django.conf import settings
4
5
  from django.core.management import call_command
5
6
  from django.db import connections
7
+ from django.db.migrations.recorder import MigrationRecorder
6
8
  from django.test.runner import _init_worker, DiscoverRunner, ParallelTestSuite
7
9
  from django.test.utils import get_unique_databases_and_mirrors, NullTimeKeeper, override_settings
8
10
  import yaml
@@ -113,6 +115,14 @@ class NautobotTestRunner(DiscoverRunner):
113
115
  command += ["--seed", settings.TEST_FACTORY_SEED]
114
116
  if self.cache_test_fixtures:
115
117
  command += ["--cache-test-fixtures"]
118
+ # Use the list of applied migrations as a unique hash to keep fixtures from differing
119
+ # branches/releases of Nautobot in separate files.
120
+ hexdigest = hashlib.shake_128(
121
+ ",".join(
122
+ sorted(f"{m.app}.{m.name}" for m in MigrationRecorder.Migration.objects.all())
123
+ ).encode("utf-8")
124
+ ).hexdigest(10)
125
+ command += ["--fixture-file", f"development/factory_dump.{hexdigest}.json"]
116
126
  with time_keeper.timed(f' Pre-populating test database "{alias}" with factory data...'):
117
127
  db_command = [*command, "--database", alias]
118
128
  call_command(*db_command)
@@ -729,8 +729,8 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
729
729
  cls.location_type = LocationType.objects.get(name="Campus")
730
730
  cls.location1 = Location.objects.filter(location_type=cls.location_type).first()
731
731
  cls.location2 = Location.objects.filter(location_type=cls.location_type).last()
732
- cls.location1.name = "Location-1"
733
- cls.location2.name = "Location-2"
732
+ cls.location1.name = "Campus Location-1"
733
+ cls.location2.name = "Campus Location-2"
734
734
  cls.location1.status = cls.location_statuses[0]
735
735
  cls.location2.status = cls.location_statuses[1]
736
736
  cls.location1.validated_save()
@@ -930,6 +930,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
930
930
  cls.prefix1 = Prefix.objects.create(
931
931
  prefix="10.0.1.0/24", namespace=cls.namespace, status=cls.prefix_statuses[0]
932
932
  )
933
+ cls.prefix1.locations.add(cls.location1, cls.location2)
933
934
  cls.ipaddr1 = IPAddress.objects.create(
934
935
  address="10.0.1.1/24", namespace=cls.namespace, status=cls.ip_statuses[0]
935
936
  )
@@ -968,6 +969,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
968
969
  cls.prefix2 = Prefix.objects.create(
969
970
  prefix="10.0.2.0/24", namespace=cls.namespace, status=cls.prefix_statuses[1]
970
971
  )
972
+ cls.prefix2.locations.add(cls.location1, cls.location2)
971
973
  cls.ipaddr2 = IPAddress.objects.create(
972
974
  address="10.0.2.1/30", namespace=cls.namespace, status=cls.ip_statuses[1]
973
975
  )
@@ -1507,9 +1509,9 @@ query {
1507
1509
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1508
1510
  def test_query_locations_filter(self):
1509
1511
  filters = (
1510
- ('name: "Location-1"', 1),
1511
- ('name: ["Location-1"]', 1),
1512
- ('name: ["Location-1", "Location-2"]', 2),
1512
+ ('name: "Campus Location-1"', 1),
1513
+ ('name: ["Campus Location-1"]', 1),
1514
+ ('name: ["Campus Location-1", "Campus Location-2"]', 2),
1513
1515
  ('name__ic: "Location"', Location.objects.filter(name__icontains="Location").count()),
1514
1516
  ('name__ic: ["Location"]', Location.objects.filter(name__icontains="Location").count()),
1515
1517
  ('name__nic: "Location"', Location.objects.exclude(name__icontains="Location").count()),
@@ -1541,6 +1543,50 @@ query {
1541
1543
  self.assertIsNone(result.errors)
1542
1544
  self.assertEqual(len(result.data["locations"]), nbr_expected_results)
1543
1545
 
1546
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1547
+ def test_query_prefixes_nested_m2m_filter(self):
1548
+ """
1549
+ Test functionality added to address https://github.com/nautobot/nautobot/issues/5906.
1550
+
1551
+ Prefix.locations is a ManyToManyField, which was not filterable in our GraphQL schema before this fix.
1552
+ """
1553
+ query = 'query { prefixes (prefix_length__gte:16) { prefix locations (location_type:["Campus"]) { name } } }'
1554
+ result = self.execute_query(query)
1555
+ self.assertIsNone(result.errors)
1556
+ found_valid_location = False
1557
+ found_invalid_location = False
1558
+ for prefix_data in result.data["prefixes"]:
1559
+ for location_data in prefix_data["locations"]:
1560
+ if location_data["name"].startswith("Campus"):
1561
+ found_valid_location = True
1562
+ else:
1563
+ print(f"Found unexpected unfiltered location {location_data['name']} under {prefix_data['prefix']}")
1564
+ found_invalid_location = True
1565
+ self.assertTrue(found_valid_location)
1566
+ self.assertFalse(found_invalid_location)
1567
+
1568
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1569
+ def test_query_locations_nested_reverse_m2m_filter(self):
1570
+ """
1571
+ Test functionality added to address https://github.com/nautobot/nautobot/issues/5906.
1572
+
1573
+ Location.prefixes is a (reverse) ManyToManyRel, which was not filterable in our GraphQL schema before this fix.
1574
+ """
1575
+ query = "query { locations { name prefixes (prefix_length:24) { prefix } } }"
1576
+ result = self.execute_query(query)
1577
+ self.assertIsNone(result.errors)
1578
+ found_valid_prefix = False
1579
+ found_invalid_prefix = False
1580
+ for location_data in result.data["locations"]:
1581
+ for prefix_data in location_data["prefixes"]:
1582
+ if prefix_data["prefix"].endswith("/24"):
1583
+ found_valid_prefix = True
1584
+ else:
1585
+ print(f"Found unexpected unfiltered prefix {prefix_data['prefix']} under {location_data['name']}")
1586
+ found_invalid_prefix = True
1587
+ self.assertTrue(found_valid_prefix)
1588
+ self.assertFalse(found_invalid_prefix)
1589
+
1544
1590
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1545
1591
  def test_query_devices_filter(self):
1546
1592
  filterset_class = DeviceFilterSet
@@ -832,9 +832,16 @@ class Device(PrimaryModel, ConfigContextModel):
832
832
  # Update Location and Rack assignment for any child Devices
833
833
  devices = Device.objects.filter(parent_bay__device=self)
834
834
  for device in devices:
835
- device.location = self.location
836
- device.rack = self.rack
837
- device.save()
835
+ save_child_device = False
836
+ if device.location != self.location:
837
+ device.location = self.location
838
+ save_child_device = True
839
+ if device.rack != self.rack:
840
+ device.rack = self.rack
841
+ save_child_device = True
842
+
843
+ if save_child_device:
844
+ device.save()
838
845
 
839
846
  def create_components(self):
840
847
  """Create device components from the device type definition."""
@@ -16,6 +16,7 @@ from nautobot.dcim.choices import (
16
16
  InterfaceTypeChoices,
17
17
  PortTypeChoices,
18
18
  PowerOutletFeedLegChoices,
19
+ SubdeviceRoleChoices,
19
20
  )
20
21
  from nautobot.dcim.models import (
21
22
  Cable,
@@ -886,6 +887,13 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
886
887
  self.device_type = DeviceType.objects.create(
887
888
  manufacturer=manufacturer,
888
889
  model="Test Device Type 1",
890
+ subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
891
+ )
892
+ self.child_devicetype = DeviceType.objects.create(
893
+ model="Child Device Type 1",
894
+ manufacturer=manufacturer,
895
+ subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
896
+ u_height=0,
889
897
  )
890
898
  self.device_role = Role.objects.get_for_model(Device).first()
891
899
  self.device_status = Status.objects.get_for_model(Device).first()
@@ -1188,6 +1196,73 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
1188
1196
  self.device_type.software_image_files.add(software_image_file)
1189
1197
  self.device.validated_save()
1190
1198
 
1199
+ def test_child_devices_are_not_saved_when_unnecessary(self):
1200
+ parent_device = Device.objects.create(
1201
+ name="Parent Device 1",
1202
+ location=self.location_3,
1203
+ device_type=self.device_type,
1204
+ role=self.device_role,
1205
+ status=self.device_status,
1206
+ )
1207
+ parent_device.validated_save()
1208
+
1209
+ child_device = Device.objects.create(
1210
+ name="Child Device 1",
1211
+ location=parent_device.location,
1212
+ device_type=self.child_devicetype,
1213
+ role=parent_device.role,
1214
+ status=self.device_status,
1215
+ )
1216
+ child_device.validated_save()
1217
+ child_mtime_before_parent_saved = str(child_device.last_updated)
1218
+
1219
+ devicebay = DeviceBay.objects.get(device=parent_device, name="Device Bay 1")
1220
+ devicebay.installed_device = child_device
1221
+ devicebay.validated_save()
1222
+
1223
+ #
1224
+ # Tests
1225
+ #
1226
+
1227
+ #
1228
+ # On a NOOP save, the child device shouldn't be updated
1229
+ parent_device.save()
1230
+
1231
+ child_mtime_after_parent_noop_save = str(Device.objects.get(name="Child Device 1").last_updated)
1232
+
1233
+ self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_noop_save)
1234
+
1235
+ #
1236
+ # On a serial number update, the child device shouldn't be updated
1237
+ parent_device.serial = "12345"
1238
+ parent_device.save()
1239
+
1240
+ child_mtime_after_parent_serial_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1241
+
1242
+ self.assertEqual(child_mtime_before_parent_saved, child_mtime_after_parent_serial_update_save)
1243
+
1244
+ #
1245
+ # If the parent rack updates, the child mtime should update.
1246
+ rack = Rack.objects.create(name="Rack 1", location=parent_device.location, status=self.device_status)
1247
+ parent_device.rack = rack
1248
+ parent_device.save()
1249
+
1250
+ child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1251
+
1252
+ self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
1253
+
1254
+ #
1255
+ # If the parent site updates, the child mtime should update
1256
+ location = Location.objects.create(
1257
+ name="New Site 1", status=self.device_status, location_type=self.location_type_3
1258
+ )
1259
+ parent_device.location = location
1260
+ parent_device.save()
1261
+
1262
+ child_mtime_after_parent_site_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
1263
+
1264
+ self.assertNotEqual(child_mtime_after_parent_rack_update_save, child_mtime_after_parent_site_update_save)
1265
+
1191
1266
 
1192
1267
  class DeviceTypeToSoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
1193
1268
  model = DeviceTypeToSoftwareImageFile
@@ -597,10 +597,17 @@ class DynamicGroup(OrganizationalModel):
597
597
  query |= filter_field.generate_query(gq_value)
598
598
 
599
599
  # For vanilla multiple-choice filters, we want all values in a set union (boolean OR)
600
- # because we want ANY of the filter values to match.
600
+ # because we want ANY of the filter values to match. Unless the filter field is explicitly
601
+ # conjoining the values, in which case we want a set intersection (boolean AND). We know this isn't right
602
+ # since the resulting query actually does tag.name == tag_1 AND tag.name == tag_2, but django_filter does
603
+ # not use Q evaluation for conjoined filters. This function is only used for the display, and the display
604
+ # is good enough to get the point across.
601
605
  elif isinstance(filter_field, django_filters.MultipleChoiceFilter):
602
606
  for v in value:
603
- query |= models.Q(**filter_field.get_filter_predicate(v))
607
+ if filter_field.conjoined:
608
+ query &= models.Q(**filter_field.get_filter_predicate(v))
609
+ else:
610
+ query |= models.Q(**filter_field.get_filter_predicate(v))
604
611
 
605
612
  # The method `get_filter_predicate()` is only available on instances or subclasses
606
613
  # of `MultipleChoiceFilter`, so we must construct a lookup if a filter is not
@@ -244,6 +244,18 @@ class RelationshipModel(models.Model):
244
244
  if relation.skip_required(cls, opposite_side):
245
245
  continue
246
246
 
247
+ if getattr(relation, f"{relation.required_on}_filter") and instance:
248
+ filterset = get_filterset_for_model(cls)
249
+ if filterset:
250
+ filter_params = getattr(relation, f"{relation.required_on}_filter")
251
+ # If the relationship is required on the model, but the object is not in the filter,
252
+ # we should allow the object to be saved, as the object is not part of the relationship.
253
+ # Example: We want a Device with a Role of Switch to be required to have a relationship
254
+ # with a Device that has a Role of Router. A Device with a Role of Printer should
255
+ # be exempt from the requirement.
256
+ if not filterset(filter_params, cls.objects.filter(id=instance.id)).qs.exists():
257
+ continue
258
+
247
259
  if relation.has_many(opposite_side):
248
260
  num_required_verbose = "at least one"
249
261
  else:
@@ -74,7 +74,7 @@ class DynamicGroupTestCase(SeleniumTestCase):
74
74
 
75
75
  # And just a cursory check to make sure that the filter worked.
76
76
  group = DynamicGroup.objects.get(name=name)
77
- self.assertEqual(group.count, len(devices))
77
+ self.assertEqual(group.count, Device.objects.filter(status__name="Active").count())
78
78
  self.assertEqual(group.filter, {"status": ["Active"]})
79
79
 
80
80
  # Verify dynamic group shows up on device detail tab
@@ -646,7 +646,7 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
646
646
 
647
647
  queryset = group.get_queryset()
648
648
 
649
- # Assert that both querysets resturn the same results
649
+ # Assert that both querysets return the same results
650
650
  group_qs = queryset.filter(multi_query)
651
651
  device_qs = Device.objects.filter(location__name__in=multi_value)
652
652
  self.assertQuerySetEqual(group_qs, device_qs)
@@ -659,6 +659,13 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
659
659
  interface_qs = Device.objects.filter(interfaces__isnull=True)
660
660
  self.assertQuerySetEqual(solo_qs, interface_qs)
661
661
 
662
+ # Tags are conjoined in the TagFilterSet, ensure that tags__name is using AND. We know this isn't right
663
+ # since the resulting query actually does tag.name == tag_1 AND tag.name == tag_2, but django_filter does
664
+ # not use Q evaluation for conjoined filters. This function is only used for the display, and the display
665
+ # is good enough to get the point across.
666
+ tags_query = group.generate_query_for_filter(filter_field=fs.filters["tags"], value=["tag_1", "tag_2"])
667
+ self.assertEqual(str(tags_query), "(AND: ('tags__name', 'tag_1'), ('tags__name', 'tag_2'))")
668
+
662
669
  # Test that a nested field_name w/ `generate_query` works as expected. This is explicitly to
663
670
  # test a regression w/ nested name-related values such as `DeviceFilterSet.manufacturer` which
664
671
  # filters on `device_type__manufacturer`.