nautobot 2.2.7__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 (278) hide show
  1. nautobot/core/graphql/generators.py +2 -2
  2. nautobot/core/graphql/schema.py +7 -4
  3. nautobot/core/tests/integration/test_general_functionality.py +36 -0
  4. nautobot/core/tests/test_graphql.py +51 -5
  5. nautobot/dcim/models/devices.py +10 -3
  6. nautobot/dcim/tests/test_models.py +75 -0
  7. nautobot/extras/models/relationships.py +12 -0
  8. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  9. nautobot/extras/tests/test_relationships.py +221 -1
  10. nautobot/project-static/docs/404.html +2 -2
  11. nautobot/project-static/docs/apps/index.html +2 -2
  12. nautobot/project-static/docs/apps/nautobot-apps.html +2 -2
  13. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css → main.76a95c52.min.css} +1 -1
  14. nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css.map → main.76a95c52.min.css.map} +1 -1
  15. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +2 -2
  16. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +2 -2
  17. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +2 -2
  18. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +2 -2
  19. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +2 -2
  20. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +2 -2
  21. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +2 -2
  22. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +2 -2
  23. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +2 -2
  24. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +2 -2
  25. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2 -2
  26. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2 -2
  27. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +2 -2
  28. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2 -2
  29. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2 -2
  30. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +2 -2
  31. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +2 -2
  32. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +2 -2
  33. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +2 -2
  34. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2 -2
  35. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +2 -2
  36. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2 -2
  37. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
  38. nautobot/project-static/docs/development/apps/api/configuration-view.html +2 -2
  39. nautobot/project-static/docs/development/apps/api/database-backend-config.html +2 -2
  40. nautobot/project-static/docs/development/apps/api/models/django-admin.html +2 -2
  41. nautobot/project-static/docs/development/apps/api/models/global-search.html +2 -2
  42. nautobot/project-static/docs/development/apps/api/models/graphql.html +2 -2
  43. nautobot/project-static/docs/development/apps/api/models/index.html +2 -2
  44. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +2 -2
  45. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +2 -2
  46. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -2
  47. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +2 -2
  48. nautobot/project-static/docs/development/apps/api/platform-features/index.html +2 -2
  49. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +2 -2
  50. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +2 -2
  51. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +2 -2
  52. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +2 -2
  53. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +2 -2
  54. nautobot/project-static/docs/development/apps/api/prometheus.html +2 -2
  55. nautobot/project-static/docs/development/apps/api/setup.html +2 -2
  56. nautobot/project-static/docs/development/apps/api/testing.html +2 -2
  57. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +2 -2
  58. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +2 -2
  59. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +2 -2
  60. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +2 -2
  61. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +2 -2
  62. nautobot/project-static/docs/development/apps/api/views/base-template.html +2 -2
  63. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +2 -2
  64. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +2 -2
  65. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +2 -2
  66. nautobot/project-static/docs/development/apps/api/views/index.html +2 -2
  67. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +2 -2
  68. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +2 -2
  69. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +2 -2
  70. nautobot/project-static/docs/development/apps/api/views/notes.html +2 -2
  71. nautobot/project-static/docs/development/apps/api/views/rest-api.html +2 -2
  72. nautobot/project-static/docs/development/apps/api/views/urls.html +2 -2
  73. nautobot/project-static/docs/development/apps/index.html +2 -2
  74. nautobot/project-static/docs/development/apps/migration/code-updates.html +2 -2
  75. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
  76. nautobot/project-static/docs/development/apps/migration/from-v1.html +2 -2
  77. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +2 -2
  78. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +2 -2
  79. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +2 -2
  80. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +2 -2
  81. nautobot/project-static/docs/development/apps/porting-from-netbox.html +2 -2
  82. nautobot/project-static/docs/development/core/application-registry.html +2 -2
  83. nautobot/project-static/docs/development/core/best-practices.html +2 -2
  84. nautobot/project-static/docs/development/core/bootstrap-ui.html +2 -2
  85. nautobot/project-static/docs/development/core/caching.html +2 -2
  86. nautobot/project-static/docs/development/core/controllers.html +2 -2
  87. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +2 -2
  88. nautobot/project-static/docs/development/core/generic-views.html +2 -2
  89. nautobot/project-static/docs/development/core/getting-started.html +2 -2
  90. nautobot/project-static/docs/development/core/homepage.html +2 -2
  91. nautobot/project-static/docs/development/core/index.html +2 -2
  92. nautobot/project-static/docs/development/core/model-checklist.html +2 -2
  93. nautobot/project-static/docs/development/core/model-features.html +2 -2
  94. nautobot/project-static/docs/development/core/natural-keys.html +2 -2
  95. nautobot/project-static/docs/development/core/navigation-menu.html +2 -2
  96. nautobot/project-static/docs/development/core/release-checklist.html +2 -2
  97. nautobot/project-static/docs/development/core/role-internals.html +2 -2
  98. nautobot/project-static/docs/development/core/settings.html +2 -2
  99. nautobot/project-static/docs/development/core/style-guide.html +2 -2
  100. nautobot/project-static/docs/development/core/templates.html +2 -2
  101. nautobot/project-static/docs/development/core/testing.html +2 -2
  102. nautobot/project-static/docs/development/core/user-preferences.html +2 -2
  103. nautobot/project-static/docs/development/index.html +2 -2
  104. nautobot/project-static/docs/development/jobs/index.html +2 -2
  105. nautobot/project-static/docs/development/jobs/migration/from-v1.html +2 -2
  106. nautobot/project-static/docs/overview/application_stack.html +2 -2
  107. nautobot/project-static/docs/overview/design_philosophy.html +2 -2
  108. nautobot/project-static/docs/overview/index.html +2 -2
  109. nautobot/project-static/docs/release-notes/index.html +2 -2
  110. nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
  111. nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
  112. nautobot/project-static/docs/release-notes/version-1.2.html +2 -2
  113. nautobot/project-static/docs/release-notes/version-1.3.html +2 -2
  114. nautobot/project-static/docs/release-notes/version-1.4.html +2 -2
  115. nautobot/project-static/docs/release-notes/version-1.5.html +2 -2
  116. nautobot/project-static/docs/release-notes/version-1.6.html +2 -2
  117. nautobot/project-static/docs/release-notes/version-2.0.html +2 -2
  118. nautobot/project-static/docs/release-notes/version-2.1.html +2 -2
  119. nautobot/project-static/docs/release-notes/version-2.2.html +254 -108
  120. nautobot/project-static/docs/requirements.txt +2 -1
  121. nautobot/project-static/docs/search/search_index.json +1 -1
  122. nautobot/project-static/docs/sitemap.xml +256 -256
  123. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  124. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +2 -2
  125. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +2 -2
  126. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +2 -2
  127. nautobot/project-static/docs/user-guide/administration/configuration/index.html +2 -2
  128. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +2 -2
  129. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +2 -2
  130. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +2 -2
  131. nautobot/project-static/docs/user-guide/administration/guides/caching.html +2 -2
  132. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +2 -2
  133. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +2 -2
  134. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +2 -2
  135. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +2 -2
  136. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +2 -2
  137. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +2 -2
  138. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
  139. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +2 -2
  140. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +2 -2
  141. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +6 -6
  142. nautobot/project-static/docs/user-guide/administration/installation/index.html +2 -2
  143. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +7 -3
  144. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +2 -2
  145. nautobot/project-static/docs/user-guide/administration/installation/services.html +2 -2
  146. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +2 -2
  147. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +2 -2
  148. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +2 -2
  149. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +2 -2
  150. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +2 -2
  151. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +2 -2
  152. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +2 -2
  153. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +2 -2
  154. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +2 -2
  155. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +2 -2
  156. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +2 -2
  157. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +2 -2
  158. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +2 -2
  159. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +2 -2
  160. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +2 -2
  161. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
  162. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +2 -2
  163. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +2 -2
  164. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +2 -2
  165. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +2 -2
  166. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +2 -2
  167. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +2 -2
  168. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +2 -2
  169. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +2 -2
  170. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +2 -2
  171. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +2 -2
  172. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +2 -2
  173. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +2 -2
  174. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +2 -2
  175. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +2 -2
  176. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +2 -2
  177. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +2 -2
  178. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +2 -2
  179. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +2 -2
  180. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +2 -2
  181. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +2 -2
  182. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +2 -2
  183. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +2 -2
  184. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +2 -2
  185. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +2 -2
  186. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +2 -2
  187. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +2 -2
  188. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +2 -2
  189. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +2 -2
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +2 -2
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +2 -2
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +2 -2
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +2 -2
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +2 -2
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +2 -2
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +2 -2
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +2 -2
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +2 -2
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +2 -2
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +2 -2
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +2 -2
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +2 -2
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +2 -2
  204. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +2 -2
  205. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +2 -2
  206. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +2 -2
  207. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +2 -2
  208. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +2 -2
  209. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +2 -2
  210. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +2 -2
  211. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +2 -2
  212. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +2 -2
  213. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +2 -2
  214. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +2 -2
  215. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +2 -2
  216. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +2 -2
  217. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +2 -2
  218. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +2 -2
  219. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +2 -2
  220. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +2 -2
  221. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +2 -2
  222. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +2 -2
  223. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +2 -2
  224. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +2 -2
  225. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +2 -2
  226. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +2 -2
  227. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +2 -2
  228. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +2 -2
  229. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +2 -2
  230. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +2 -2
  231. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +2 -2
  232. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +2 -2
  233. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +2 -2
  234. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +2 -2
  235. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +2 -2
  236. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +2 -2
  237. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +2 -2
  238. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +2 -2
  239. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +2 -2
  240. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +2 -2
  241. nautobot/project-static/docs/user-guide/index.html +2 -2
  242. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +2 -2
  243. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +2 -2
  244. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +2 -2
  245. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +2 -2
  246. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +2 -2
  247. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +2 -2
  248. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +2 -2
  249. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +2 -2
  250. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +2 -2
  251. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +2 -2
  252. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +2 -2
  253. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +2 -2
  254. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +2 -2
  255. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +2 -2
  256. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +2 -2
  257. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +2 -2
  258. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +2 -2
  259. nautobot/project-static/docs/user-guide/platform-functionality/note.html +2 -2
  260. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +2 -2
  261. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +2 -2
  262. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +2 -2
  263. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +2 -2
  264. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +2 -2
  265. nautobot/project-static/docs/user-guide/platform-functionality/role.html +2 -2
  266. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +2 -2
  267. nautobot/project-static/docs/user-guide/platform-functionality/status.html +2 -2
  268. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +2 -2
  269. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +2 -2
  270. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +2 -2
  271. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +2 -2
  272. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +2 -2
  273. {nautobot-2.2.7.dist-info → nautobot-2.2.8.dist-info}/METADATA +3 -3
  274. {nautobot-2.2.7.dist-info → nautobot-2.2.8.dist-info}/RECORD +278 -277
  275. {nautobot-2.2.7.dist-info → nautobot-2.2.8.dist-info}/LICENSE.txt +0 -0
  276. {nautobot-2.2.7.dist-info → nautobot-2.2.8.dist-info}/NOTICE +0 -0
  277. {nautobot-2.2.7.dist-info → nautobot-2.2.8.dist-info}/WHEEL +0 -0
  278. {nautobot-2.2.7.dist-info → nautobot-2.2.8.dist-info}/entry_points.txt +0 -0
@@ -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"))
@@ -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
@@ -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
@@ -15,9 +15,11 @@ from nautobot.core.tables import RelationshipColumn
15
15
  from nautobot.core.testing import TestCase
16
16
  from nautobot.core.testing.models import ModelTestCases
17
17
  from nautobot.core.utils.lookup import get_route_for_model
18
+ from nautobot.dcim.forms import DeviceForm
18
19
  from nautobot.dcim.models import (
19
20
  Controller,
20
21
  Device,
22
+ DeviceType,
21
23
  DeviceTypeToSoftwareImageFile,
22
24
  Location,
23
25
  LocationType,
@@ -27,7 +29,7 @@ from nautobot.dcim.models import (
27
29
  from nautobot.dcim.tables import LocationTable
28
30
  from nautobot.dcim.tests.test_views import create_test_device
29
31
  from nautobot.extras.choices import RelationshipRequiredSideChoices, RelationshipSideChoices, RelationshipTypeChoices
30
- from nautobot.extras.models import Relationship, RelationshipAssociation, Status
32
+ from nautobot.extras.models import Relationship, RelationshipAssociation, Role, Status
31
33
  from nautobot.ipam.models import VLAN, VLANGroup
32
34
 
33
35
 
@@ -486,6 +488,224 @@ class RelationshipTest(RelationshipBaseTest, ModelTestCases.BaseModelTestCase):
486
488
  with self.assertNumQueries(0):
487
489
  manager_method(Location)
488
490
 
491
+ def test_required_related_object_errors(self):
492
+ """
493
+ Confirm that the fix in https://github.com/nautobot/nautobot/pull/5570 is working as expected
494
+ """
495
+ device_ct = ContentType.objects.get_for_model(Device)
496
+ status = Status.objects.get_for_model(Device).first()
497
+ device_type = DeviceType.objects.exclude(manufacturer__isnull=True).first()
498
+ # Create a Device with role Role 1
499
+ role_1 = Role.objects.create(name="Role 1")
500
+ role_1.content_types.add(ContentType.objects.get_for_model(Device))
501
+ device_1 = Device.objects.create(
502
+ device_type=device_type, role=role_1, name="Device 1", location=self.locations[0], status=status
503
+ )
504
+ # Create a Device with role Role 2
505
+ role_2 = Role.objects.create(name="Role 2")
506
+ role_2.content_types.add(ContentType.objects.get_for_model(Device))
507
+ device_2 = Device.objects.create(
508
+ device_type=device_type, role=role_2, name="Device 2", location=self.locations[0], status=status
509
+ )
510
+ # Create a Device with role Role 3
511
+ role_3 = Role.objects.create(name="Role 3")
512
+ role_3.content_types.add(ContentType.objects.get_for_model(Device))
513
+ device_3 = Device.objects.create(
514
+ device_type=device_type, role=role_3, name="Device 3", location=self.locations[0], status=status
515
+ )
516
+ # Create a one-to-many relationship with destination required, source filter: {"role": ["Role 1"]}
517
+ # and destination filter {"role": ["Role 2"]}
518
+ relationship = Relationship.objects.create(
519
+ label="Device to Devices",
520
+ key="device_to_devices",
521
+ source_type=device_ct,
522
+ source_filter={"role": ["Role 1"]},
523
+ destination_type=device_ct,
524
+ destination_filter={"role": ["Role 2"]},
525
+ type=RelationshipTypeChoices.TYPE_ONE_TO_MANY,
526
+ required_on="destination",
527
+ )
528
+ # Attempt to update device_3 which will not be in the queryset filtered by the destination filter
529
+ # Assert that the form is valid and no ValueError is raised.
530
+ update_status = Status.objects.get_for_model(Device).last()
531
+ update_data_for_device_3 = {
532
+ "location": device_3.location.pk,
533
+ "device_type": device_3.device_type.pk,
534
+ "role": device_3.role.pk,
535
+ "name": device_3.name,
536
+ "status": update_status.pk,
537
+ }
538
+ form = DeviceForm(instance=device_3, data=update_data_for_device_3)
539
+ self.assertTrue(form.is_valid())
540
+ # Attempt to update device_1 which will not be in the destination filter,
541
+ # but is in the source filter.
542
+ update_data_for_device_1 = {
543
+ "location": device_1.location.pk,
544
+ "device_type": device_1.device_type.pk,
545
+ "role": device_1.role.pk,
546
+ "name": device_1.name,
547
+ "status": update_status.pk,
548
+ }
549
+ form2 = DeviceForm(instance=device_1, data=update_data_for_device_1)
550
+ self.assertTrue(form2.is_valid())
551
+ # Attempt to update device_2 which will be in the destination filter, so it should
552
+ # require the relationship.
553
+ update_data_for_device_2 = {
554
+ "location": device_2.location.pk,
555
+ "device_type": device_2.device_type.pk,
556
+ "role": device_2.role.pk,
557
+ "name": "Device 2",
558
+ "status": update_status.pk,
559
+ }
560
+ form3 = DeviceForm(instance=device_2, data=update_data_for_device_2)
561
+ self.assertFalse(form3.is_valid())
562
+ # Device 1 has a relationship to Device 2
563
+ update_data_for_device_1 = {
564
+ "location": device_1.location.pk,
565
+ "device_type": device_1.device_type.pk,
566
+ "role": device_1.role.pk,
567
+ "name": device_1.name,
568
+ "status": update_status.pk,
569
+ "cr_device_to_devices__destination": [device_2.pk],
570
+ }
571
+ form4 = DeviceForm(instance=device_1, data=update_data_for_device_1)
572
+ self.assertTrue(form4.is_valid())
573
+ form4.save()
574
+ # Device 2 has a relationship to Device 1, form should validate and save.
575
+ update_data_for_device_2 = {
576
+ "location": device_2.location.pk,
577
+ "device_type": device_2.device_type.pk,
578
+ "role": device_2.role.pk,
579
+ "name": "Device 2",
580
+ "status": update_status.pk,
581
+ "cr_device_to_devices__source": device_1.pk,
582
+ }
583
+ form5 = DeviceForm(instance=device_2, data=update_data_for_device_2)
584
+ self.assertTrue(form5.is_valid())
585
+ form5.save()
586
+ # Device 2 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
587
+ update_data_for_device_2 = {
588
+ "location": device_2.location.pk,
589
+ "device_type": device_2.device_type.pk,
590
+ "role": device_2.role.pk,
591
+ "name": "Device 2",
592
+ "status": update_status.pk,
593
+ "cr_device_to_devices__source": device_3.pk,
594
+ }
595
+ form6 = DeviceForm(instance=device_2, data=update_data_for_device_2)
596
+ with self.assertRaises(ValidationError):
597
+ form6.save()
598
+ # Device 1 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
599
+ update_data_for_device_1 = {
600
+ "location": device_1.location.pk,
601
+ "device_type": device_1.device_type.pk,
602
+ "role": device_1.role.pk,
603
+ "name": "Device 1",
604
+ "status": update_status.pk,
605
+ "cr_device_to_devices__destination": [
606
+ device_3.pk,
607
+ ],
608
+ }
609
+ form6 = DeviceForm(instance=device_1, data=update_data_for_device_1)
610
+ with self.assertRaises(ValidationError):
611
+ form6.save()
612
+
613
+ relationship.required_on = "source"
614
+ relationship.save()
615
+ # Attempt to update device_3 which will not be in the queryset filtered by the destination filter
616
+ # Assert that the form is valid and no ValueError is raised. This ensures that an object that
617
+ # does not take part in any relationships can still be updated, addressing issue #5569.
618
+ update_status = Status.objects.get_for_model(Device).last()
619
+ update_data_for_device_3 = {
620
+ "location": device_3.location.pk,
621
+ "device_type": device_3.device_type.pk,
622
+ "role": device_3.role.pk,
623
+ "name": device_3.name,
624
+ "status": update_status.pk,
625
+ }
626
+ form = DeviceForm(instance=device_3, data=update_data_for_device_3)
627
+ self.assertTrue(form.is_valid())
628
+ # Attempt to update device_1 which will not be in the destination filter,
629
+ # but is in the source filter. Should fail as device_to_devices is required.
630
+ device_1.delete()
631
+ device_1 = Device.objects.create(
632
+ device_type=device_type, role=role_1, name="Device 1", location=self.locations[0], status=status
633
+ )
634
+ update_data_for_device_1 = {
635
+ "location": device_1.location.pk,
636
+ "device_type": device_1.device_type.pk,
637
+ "role": device_1.role.pk,
638
+ "name": device_1.name,
639
+ "status": update_status.pk,
640
+ }
641
+ form2 = DeviceForm(instance=device_1, data=update_data_for_device_1)
642
+ self.assertFalse(form2.is_valid())
643
+ # Attempt to update device_2 which will be in the destination filter, which should not require the
644
+ # relationship anymore.
645
+ device_2.delete()
646
+ device_2 = Device.objects.create(
647
+ device_type=device_type, role=role_2, name="Device 2", location=self.locations[0], status=status
648
+ )
649
+ update_data_for_device_2 = {
650
+ "location": device_2.location.pk,
651
+ "device_type": device_2.device_type.pk,
652
+ "role": device_2.role.pk,
653
+ "name": "Device 2",
654
+ "status": update_status.pk,
655
+ }
656
+ form3 = DeviceForm(instance=device_2, data=update_data_for_device_2)
657
+ self.assertTrue(form3.is_valid())
658
+ # Device 1 has a relationship to Device 2
659
+ update_data_for_device_1 = {
660
+ "location": device_1.location.pk,
661
+ "device_type": device_1.device_type.pk,
662
+ "role": device_1.role.pk,
663
+ "name": device_1.name,
664
+ "status": update_status.pk,
665
+ "cr_device_to_devices__destination": [device_2.pk],
666
+ }
667
+ form4 = DeviceForm(instance=device_1, data=update_data_for_device_1)
668
+ self.assertTrue(form4.is_valid())
669
+ form4.save()
670
+ # Device 2 has a relationship to Device 1, form should validate and save.
671
+ update_data_for_device_2 = {
672
+ "location": device_2.location.pk,
673
+ "device_type": device_2.device_type.pk,
674
+ "role": device_2.role.pk,
675
+ "name": "Device 2",
676
+ "status": update_status.pk,
677
+ "cr_device_to_devices__source": device_1.pk,
678
+ }
679
+ form5 = DeviceForm(instance=device_2, data=update_data_for_device_2)
680
+ self.assertTrue(form5.is_valid())
681
+ form5.save()
682
+ # Device 2 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
683
+ update_data_for_device_2 = {
684
+ "location": device_2.location.pk,
685
+ "device_type": device_2.device_type.pk,
686
+ "role": device_2.role.pk,
687
+ "name": "Device 2",
688
+ "status": update_status.pk,
689
+ "cr_device_to_devices__source": device_3.pk,
690
+ }
691
+ form6 = DeviceForm(instance=device_2, data=update_data_for_device_2)
692
+ with self.assertRaises(ValidationError):
693
+ form6.save()
694
+ # Device 1 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
695
+ update_data_for_device_1 = {
696
+ "location": device_1.location.pk,
697
+ "device_type": device_1.device_type.pk,
698
+ "role": device_1.role.pk,
699
+ "name": "Device 1",
700
+ "status": update_status.pk,
701
+ "cr_device_to_devices__destination": [
702
+ device_3.pk,
703
+ ],
704
+ }
705
+ form6 = DeviceForm(instance=device_1, data=update_data_for_device_1)
706
+ with self.assertRaises(ValidationError):
707
+ form6.save()
708
+
489
709
 
490
710
  class RelationshipAssociationTest(RelationshipBaseTest, ModelTestCases.BaseModelTestCase):
491
711
  model = RelationshipAssociation
@@ -12,7 +12,7 @@
12
12
 
13
13
 
14
14
  <link rel="icon" href="/projects/core/en/stable/assets/favicon.ico">
15
- <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.28">
15
+ <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
16
16
 
17
17
 
18
18
 
@@ -20,7 +20,7 @@
20
20
 
21
21
 
22
22
 
23
- <link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/main.6543a935.min.css">
23
+ <link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/main.76a95c52.min.css">
24
24
 
25
25
 
26
26
  <link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/palette.06af60db.min.css">
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
  <link rel="icon" href="../assets/favicon.ico">
21
- <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.28">
21
+ <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
22
22
 
23
23
 
24
24
 
@@ -26,7 +26,7 @@
26
26
 
27
27
 
28
28
 
29
- <link rel="stylesheet" href="../assets/stylesheets/main.6543a935.min.css">
29
+ <link rel="stylesheet" href="../assets/stylesheets/main.76a95c52.min.css">
30
30
 
31
31
 
32
32
  <link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
@@ -16,7 +16,7 @@
16
16
 
17
17
 
18
18
  <link rel="icon" href="../assets/favicon.ico">
19
- <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.28">
19
+ <meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
20
20
 
21
21
 
22
22
 
@@ -24,7 +24,7 @@
24
24
 
25
25
 
26
26
 
27
- <link rel="stylesheet" href="../assets/stylesheets/main.6543a935.min.css">
27
+ <link rel="stylesheet" href="../assets/stylesheets/main.76a95c52.min.css">
28
28
 
29
29
 
30
30
  <link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">