nautobot 2.4.1__py3-none-any.whl → 2.4.2__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 (406) hide show
  1. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +43 -0
  2. nautobot/circuits/tests/integration/test_relationships.py +1 -1
  3. nautobot/core/apps/__init__.py +0 -5
  4. nautobot/core/templates/generic/object_bulk_destroy.html +1 -1
  5. nautobot/core/testing/integration.py +437 -10
  6. nautobot/core/tests/test_jobs.py +34 -2
  7. nautobot/core/utils/git.py +7 -2
  8. nautobot/core/views/generic.py +1 -1
  9. nautobot/core/views/mixins.py +13 -6
  10. nautobot/core/views/utils.py +2 -2
  11. nautobot/dcim/forms.py +12 -0
  12. nautobot/dcim/tables/devices.py +2 -1
  13. nautobot/dcim/templates/dcim/cable.html +1 -1
  14. nautobot/dcim/templates/dcim/device/base.html +1 -1
  15. nautobot/dcim/templates/dcim/device.html +2 -2
  16. nautobot/dcim/templates/dcim/device_component.html +1 -1
  17. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  18. nautobot/dcim/templates/dcim/location.html +1 -1
  19. nautobot/dcim/templates/dcim/locationtype.html +1 -1
  20. nautobot/dcim/templates/dcim/locationtype_retrieve.html +1 -1
  21. nautobot/dcim/templates/dcim/manufacturer.html +1 -1
  22. nautobot/dcim/templates/dcim/platform.html +1 -1
  23. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  24. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  25. nautobot/dcim/templates/dcim/rack.html +1 -1
  26. nautobot/dcim/templates/dcim/rackgroup.html +1 -1
  27. nautobot/dcim/templates/dcim/rackreservation.html +2 -2
  28. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  29. nautobot/dcim/tests/integration/test_device_bulk_operations.py +30 -0
  30. nautobot/dcim/tests/integration/test_location_bulk_operations.py +43 -0
  31. nautobot/dcim/tests/test_views.py +9 -1
  32. nautobot/dcim/views.py +12 -15
  33. nautobot/extras/api/serializers.py +33 -0
  34. nautobot/extras/api/views.py +11 -3
  35. nautobot/extras/constants.py +1 -0
  36. nautobot/extras/datasources/git.py +125 -0
  37. nautobot/extras/migrations/0122_add_graphqlquery_owner_content_type.py +34 -0
  38. nautobot/extras/models/customfields.py +29 -12
  39. nautobot/extras/models/datasources.py +85 -0
  40. nautobot/extras/models/models.py +15 -0
  41. nautobot/extras/models/relationships.py +17 -5
  42. nautobot/extras/signals.py +15 -1
  43. nautobot/extras/templates/extras/computedfield.html +1 -1
  44. nautobot/extras/templates/extras/configcontext.html +1 -1
  45. nautobot/extras/templates/extras/configcontextschema.html +1 -1
  46. nautobot/extras/templates/extras/customfield.html +1 -1
  47. nautobot/extras/templates/extras/customlink.html +1 -1
  48. nautobot/extras/templates/extras/dynamicgroup.html +1 -1
  49. nautobot/extras/templates/extras/exporttemplate.html +1 -1
  50. nautobot/extras/templates/extras/gitrepository.html +1 -1
  51. nautobot/extras/templates/extras/graphqlquery.html +1 -1
  52. nautobot/extras/templates/extras/job_detail.html +1 -1
  53. nautobot/extras/templates/extras/jobbutton_retrieve.html +1 -1
  54. nautobot/extras/templates/extras/jobhook.html +1 -1
  55. nautobot/extras/templates/extras/jobresult.html +1 -1
  56. nautobot/extras/templates/extras/objectchange.html +1 -1
  57. nautobot/extras/templates/extras/plugin_detail.html +1 -1
  58. nautobot/extras/templates/extras/relationship.html +1 -63
  59. nautobot/extras/templates/extras/role_retrieve.html +1 -1
  60. nautobot/extras/templates/extras/scheduledjob.html +1 -1
  61. nautobot/extras/templates/extras/secret.html +1 -1
  62. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  63. nautobot/extras/templates/extras/status.html +1 -1
  64. nautobot/extras/templates/extras/tag.html +1 -1
  65. nautobot/extras/templates/extras/webhook.html +1 -1
  66. nautobot/extras/tests/git_data/01-valid-files/graphql_queries/device_interfaces.gql +8 -0
  67. nautobot/extras/tests/git_data/01-valid-files/graphql_queries/device_names.gql +5 -0
  68. nautobot/extras/tests/git_data/02-invalid-files/graphql_queries/bad_device_names.gql +5 -0
  69. nautobot/extras/tests/git_helper.py +9 -1
  70. nautobot/extras/tests/integration/__init__.py +29 -16
  71. nautobot/extras/tests/test_api.py +6 -0
  72. nautobot/extras/tests/test_customfields.py +49 -51
  73. nautobot/extras/tests/test_datasources.py +27 -0
  74. nautobot/extras/tests/test_models.py +283 -0
  75. nautobot/extras/tests/test_utils.py +22 -1
  76. nautobot/extras/utils.py +17 -8
  77. nautobot/extras/views.py +55 -12
  78. nautobot/ipam/models.py +8 -2
  79. nautobot/ipam/tables.py +2 -2
  80. nautobot/ipam/templates/ipam/ipaddress.html +1 -1
  81. nautobot/ipam/templates/ipam/prefix.html +1 -1
  82. nautobot/ipam/templates/ipam/rir.html +1 -1
  83. nautobot/ipam/templates/ipam/routetarget.html +1 -1
  84. nautobot/ipam/templates/ipam/service.html +1 -1
  85. nautobot/ipam/templates/ipam/vlan.html +1 -1
  86. nautobot/ipam/templates/ipam/vlangroup.html +1 -1
  87. nautobot/ipam/templates/ipam/vrf.html +1 -1
  88. nautobot/ipam/tests/test_models.py +24 -0
  89. nautobot/ipam/tests/test_utils.py +41 -2
  90. nautobot/ipam/utils/__init__.py +18 -11
  91. nautobot/project-static/docs/404.html +87 -12
  92. nautobot/project-static/docs/apps/index.html +87 -12
  93. nautobot/project-static/docs/apps/nautobot-apps.html +87 -12
  94. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js → bundle.60a45f97.min.js} +1 -1
  95. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js.map → bundle.60a45f97.min.js.map} +1 -1
  96. nautobot/project-static/docs/assets/javascripts/workers/{search.6ce7567c.min.js → search.f8cc74c7.min.js} +1 -1
  97. nautobot/project-static/docs/assets/javascripts/workers/{search.6ce7567c.min.js.map → search.f8cc74c7.min.js.map} +1 -1
  98. nautobot/project-static/docs/assets/stylesheets/{main.6f8fc17f.min.css → main.a40c8224.min.css} +1 -1
  99. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +87 -12
  100. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +87 -12
  101. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +87 -12
  102. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +87 -12
  103. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +87 -12
  104. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +87 -12
  105. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +87 -12
  106. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +87 -12
  107. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +87 -12
  108. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +87 -12
  109. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +87 -12
  110. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +87 -12
  111. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +87 -12
  112. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +87 -12
  113. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +87 -12
  114. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +87 -12
  115. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +87 -12
  116. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +87 -12
  117. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +87 -12
  118. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +87 -12
  119. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +87 -12
  120. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +87 -12
  121. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +177 -20
  122. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +114 -17
  123. nautobot/project-static/docs/development/apps/api/configuration-view.html +87 -12
  124. nautobot/project-static/docs/development/apps/api/database-backend-config.html +87 -12
  125. nautobot/project-static/docs/development/apps/api/models/django-admin.html +87 -12
  126. nautobot/project-static/docs/development/apps/api/models/global-search.html +87 -12
  127. nautobot/project-static/docs/development/apps/api/models/graphql.html +87 -12
  128. nautobot/project-static/docs/development/apps/api/models/index.html +87 -12
  129. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +87 -12
  130. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +87 -12
  131. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +87 -12
  132. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +87 -12
  133. nautobot/project-static/docs/development/apps/api/platform-features/index.html +87 -12
  134. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +87 -12
  135. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +87 -12
  136. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +87 -12
  137. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +87 -12
  138. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +87 -12
  139. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +87 -12
  140. nautobot/project-static/docs/development/apps/api/prometheus.html +87 -12
  141. nautobot/project-static/docs/development/apps/api/setup.html +87 -12
  142. nautobot/project-static/docs/development/apps/api/testing.html +87 -12
  143. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +87 -12
  144. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +87 -12
  145. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +87 -12
  146. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +87 -12
  147. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +87 -12
  148. nautobot/project-static/docs/development/apps/api/views/base-template.html +87 -12
  149. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +87 -12
  150. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +87 -12
  151. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +87 -12
  152. nautobot/project-static/docs/development/apps/api/views/index.html +87 -12
  153. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +87 -12
  154. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +87 -12
  155. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +87 -12
  156. nautobot/project-static/docs/development/apps/api/views/notes.html +87 -12
  157. nautobot/project-static/docs/development/apps/api/views/rest-api.html +87 -12
  158. nautobot/project-static/docs/development/apps/api/views/urls.html +87 -12
  159. nautobot/project-static/docs/development/apps/index.html +87 -12
  160. nautobot/project-static/docs/development/apps/migration/code-updates.html +87 -12
  161. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +87 -12
  162. nautobot/project-static/docs/development/apps/migration/from-v1.html +87 -12
  163. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +87 -12
  164. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +87 -12
  165. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +87 -12
  166. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +87 -12
  167. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +87 -12
  168. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +87 -12
  169. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +88 -13
  170. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +87 -12
  171. nautobot/project-static/docs/development/apps/porting-from-netbox.html +87 -12
  172. nautobot/project-static/docs/development/core/application-registry.html +87 -12
  173. nautobot/project-static/docs/development/core/best-practices.html +87 -12
  174. nautobot/project-static/docs/development/core/bootstrap-ui.html +87 -12
  175. nautobot/project-static/docs/development/core/caching.html +87 -12
  176. nautobot/project-static/docs/development/core/controllers.html +87 -12
  177. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +87 -12
  178. nautobot/project-static/docs/development/core/generic-views.html +87 -12
  179. nautobot/project-static/docs/development/core/getting-started.html +87 -12
  180. nautobot/project-static/docs/development/core/homepage.html +87 -12
  181. nautobot/project-static/docs/development/core/index.html +87 -12
  182. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +87 -12
  183. nautobot/project-static/docs/development/core/model-checklist.html +87 -12
  184. nautobot/project-static/docs/development/core/model-features.html +87 -12
  185. nautobot/project-static/docs/development/core/natural-keys.html +87 -12
  186. nautobot/project-static/docs/development/core/navigation-menu.html +87 -12
  187. nautobot/project-static/docs/development/core/release-checklist.html +87 -12
  188. nautobot/project-static/docs/development/core/role-internals.html +87 -12
  189. nautobot/project-static/docs/development/core/settings.html +87 -12
  190. nautobot/project-static/docs/development/core/style-guide.html +87 -12
  191. nautobot/project-static/docs/development/core/templates.html +88 -13
  192. nautobot/project-static/docs/development/core/testing.html +87 -12
  193. nautobot/project-static/docs/development/core/ui-component-framework.html +87 -12
  194. nautobot/project-static/docs/development/core/user-preferences.html +87 -12
  195. nautobot/project-static/docs/development/index.html +87 -12
  196. nautobot/project-static/docs/development/jobs/index.html +87 -12
  197. nautobot/project-static/docs/development/jobs/migration/from-v1.html +87 -12
  198. nautobot/project-static/docs/index.html +87 -12
  199. nautobot/project-static/docs/overview/application_stack.html +87 -12
  200. nautobot/project-static/docs/overview/design_philosophy.html +87 -12
  201. nautobot/project-static/docs/release-notes/index.html +87 -12
  202. nautobot/project-static/docs/release-notes/version-1.0.html +87 -12
  203. nautobot/project-static/docs/release-notes/version-1.1.html +87 -12
  204. nautobot/project-static/docs/release-notes/version-1.2.html +87 -12
  205. nautobot/project-static/docs/release-notes/version-1.3.html +87 -12
  206. nautobot/project-static/docs/release-notes/version-1.4.html +87 -12
  207. nautobot/project-static/docs/release-notes/version-1.5.html +87 -12
  208. nautobot/project-static/docs/release-notes/version-1.6.html +87 -12
  209. nautobot/project-static/docs/release-notes/version-2.0.html +87 -12
  210. nautobot/project-static/docs/release-notes/version-2.1.html +87 -12
  211. nautobot/project-static/docs/release-notes/version-2.2.html +87 -12
  212. nautobot/project-static/docs/release-notes/version-2.3.html +87 -12
  213. nautobot/project-static/docs/release-notes/version-2.4.html +277 -12
  214. nautobot/project-static/docs/requirements.txt +1 -1
  215. nautobot/project-static/docs/search/search_index.json +1 -1
  216. nautobot/project-static/docs/sitemap.xml +296 -288
  217. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  218. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +87 -12
  219. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +87 -12
  220. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +87 -12
  221. nautobot/project-static/docs/user-guide/administration/configuration/index.html +87 -12
  222. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +87 -12
  223. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +87 -12
  224. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +87 -12
  225. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +90 -15
  226. nautobot/project-static/docs/user-guide/administration/guides/docker.html +87 -12
  227. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +87 -12
  228. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +87 -12
  229. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +87 -12
  230. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +87 -12
  231. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +87 -12
  232. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +87 -12
  233. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +87 -12
  234. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +87 -12
  235. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +87 -12
  236. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +87 -12
  237. nautobot/project-static/docs/user-guide/administration/installation/index.html +87 -12
  238. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +87 -12
  239. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +87 -12
  240. nautobot/project-static/docs/user-guide/administration/installation/services.html +87 -12
  241. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +87 -12
  242. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +87 -12
  243. nautobot/project-static/docs/user-guide/administration/security/index.html +9420 -0
  244. nautobot/project-static/docs/user-guide/administration/security/notices.html +9843 -0
  245. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +87 -12
  246. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +87 -12
  247. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +87 -12
  248. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +87 -12
  249. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +87 -12
  250. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +87 -12
  251. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +87 -12
  252. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +87 -12
  253. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +87 -12
  254. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +87 -12
  255. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +87 -12
  256. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +87 -12
  257. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +87 -12
  258. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +87 -12
  259. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +87 -12
  260. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +87 -12
  261. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +87 -12
  262. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +87 -12
  263. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +87 -12
  264. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +87 -12
  265. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +87 -12
  266. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +87 -12
  267. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +87 -12
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +87 -12
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +87 -12
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +87 -12
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +87 -12
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +87 -12
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +87 -12
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +87 -12
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +87 -12
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +87 -12
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +87 -12
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +87 -12
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +87 -12
  280. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +87 -12
  281. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +87 -12
  282. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +87 -12
  283. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +87 -12
  284. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +87 -12
  285. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +87 -12
  286. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +87 -12
  287. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +87 -12
  288. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +87 -12
  289. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +87 -12
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +87 -12
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +87 -12
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +87 -12
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +87 -12
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +87 -12
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +87 -12
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +87 -12
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +87 -12
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +87 -12
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +87 -12
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +87 -12
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +87 -12
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +87 -12
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +87 -12
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +87 -12
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +87 -12
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +87 -12
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +87 -12
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +87 -12
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +87 -12
  310. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +87 -12
  311. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +87 -12
  312. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +87 -12
  313. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +87 -12
  314. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +87 -12
  315. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +87 -12
  316. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +87 -12
  317. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +87 -12
  318. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +87 -12
  319. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +87 -12
  320. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +87 -12
  321. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +87 -12
  322. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +87 -12
  323. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +87 -12
  324. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +87 -12
  325. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +87 -12
  326. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +87 -12
  327. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +87 -12
  328. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +87 -12
  329. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +87 -12
  330. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +87 -12
  331. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +87 -12
  332. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +87 -12
  333. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +87 -12
  334. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +87 -12
  335. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +87 -12
  336. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +87 -12
  337. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +87 -12
  338. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +87 -12
  339. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +90 -15
  340. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +87 -12
  341. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +87 -12
  342. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +87 -12
  343. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +87 -12
  344. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +87 -12
  345. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +87 -12
  346. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +187 -29
  347. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +87 -12
  348. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +87 -12
  349. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +87 -12
  350. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +87 -12
  351. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +87 -12
  352. nautobot/project-static/docs/user-guide/index.html +87 -12
  353. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +87 -12
  354. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +87 -12
  355. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +87 -12
  356. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +87 -12
  357. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +87 -12
  358. nautobot/project-static/docs/user-guide/platform-functionality/events.html +87 -12
  359. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +87 -12
  360. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +87 -12
  361. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +407 -14
  362. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +87 -12
  363. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +87 -12
  364. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +87 -12
  365. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +87 -12
  366. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +87 -12
  367. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +87 -12
  368. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +87 -12
  369. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +87 -12
  370. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +87 -12
  371. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +87 -12
  372. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +87 -12
  373. nautobot/project-static/docs/user-guide/platform-functionality/note.html +87 -12
  374. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +87 -12
  375. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +87 -12
  376. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +87 -12
  377. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +87 -12
  378. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +87 -12
  379. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +87 -12
  380. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +87 -12
  381. nautobot/project-static/docs/user-guide/platform-functionality/role.html +87 -12
  382. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +87 -12
  383. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +87 -12
  384. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +87 -12
  385. nautobot/project-static/docs/user-guide/platform-functionality/status.html +87 -12
  386. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +87 -12
  387. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +87 -12
  388. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +87 -12
  389. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +87 -12
  390. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +87 -12
  391. nautobot/tenancy/templates/tenancy/tenant.html +1 -2
  392. nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
  393. nautobot/virtualization/templates/virtualization/cluster.html +1 -1
  394. nautobot/virtualization/templates/virtualization/clustergroup.html +1 -1
  395. nautobot/virtualization/templates/virtualization/clustertype.html +1 -1
  396. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  397. nautobot/virtualization/templates/virtualization/vminterface.html +1 -1
  398. {nautobot-2.4.1.dist-info → nautobot-2.4.2.dist-info}/METADATA +5 -5
  399. {nautobot-2.4.1.dist-info → nautobot-2.4.2.dist-info}/RECORD +404 -397
  400. nautobot/dcim/tests/integration/test_device_bulk_delete.py +0 -189
  401. nautobot/dcim/tests/integration/test_device_bulk_edit.py +0 -181
  402. /nautobot/project-static/docs/assets/stylesheets/{main.6f8fc17f.min.css.map → main.a40c8224.min.css.map} +0 -0
  403. {nautobot-2.4.1.dist-info → nautobot-2.4.2.dist-info}/LICENSE.txt +0 -0
  404. {nautobot-2.4.1.dist-info → nautobot-2.4.2.dist-info}/NOTICE +0 -0
  405. {nautobot-2.4.1.dist-info → nautobot-2.4.2.dist-info}/WHEEL +0 -0
  406. {nautobot-2.4.1.dist-info → nautobot-2.4.2.dist-info}/entry_points.txt +0 -0
@@ -1207,6 +1207,10 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
1207
1207
  url = reverse("extras-api:gitrepository-sync", kwargs={"pk": self.repos[0].id})
1208
1208
  response = self.client.post(url, format="json", **self.header)
1209
1209
  self.assertHttpStatus(response, status.HTTP_200_OK)
1210
+ self.assertIn("message", response.data)
1211
+ self.assertIn("job_result", response.data)
1212
+ self.assertEqual(response.data["message"], f"Repository {self.repos[0].name} sync job added to queue.")
1213
+ self.assertIsInstance(response.data["job_result"], dict)
1210
1214
 
1211
1215
  def test_create_with_app_provided_contents(self):
1212
1216
  """Test that `provided_contents` published by an App works."""
@@ -1241,6 +1245,8 @@ class GraphQLQueryTest(APIViewTestCases.APIViewTestCase):
1241
1245
  },
1242
1246
  ]
1243
1247
 
1248
+ choices_fields = ["owner_content_type"]
1249
+
1244
1250
  @classmethod
1245
1251
  def setUpTestData(cls):
1246
1252
  cls.graphqlqueries = (
@@ -53,12 +53,12 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
53
53
 
54
54
  instance.refresh_from_db()
55
55
  instance.key = "custom_field_2"
56
- with self.assertRaises(ValidationError):
56
+ with self.assertRaisesRegex(ValidationError, "Key cannot be changed once created"):
57
57
  instance.validated_save()
58
58
 
59
59
  instance.refresh_from_db()
60
60
  instance.type = CustomFieldTypeChoices.TYPE_SELECT
61
- with self.assertRaises(ValidationError):
61
+ with self.assertRaisesRegex(ValidationError, "Type cannot be changed once created"):
62
62
  instance.validated_save()
63
63
 
64
64
  def test_simple_fields(self):
@@ -165,9 +165,14 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
165
165
  cf.save()
166
166
  cf.content_types.set([obj_type])
167
167
 
168
- CustomFieldChoice.objects.create(custom_field=cf, value="Option A")
169
- CustomFieldChoice.objects.create(custom_field=cf, value="Option B")
170
- CustomFieldChoice.objects.create(custom_field=cf, value="Option C")
168
+ CustomFieldChoice.objects.create(custom_field=cf, value="Option A", weight=100)
169
+ self.assertEqual(["Option A"], cf.choices)
170
+ CustomFieldChoice.objects.create(custom_field=cf, value="Option B", weight=200)
171
+ self.assertEqual(["Option A", "Option B"], cf.choices)
172
+ CustomFieldChoice.objects.create(custom_field=cf, value="Option C", weight=300)
173
+ self.assertEqual(["Option A", "Option B", "Option C"], cf.choices)
174
+ with self.assertNumQueries(0): # verify caching
175
+ self.assertEqual(["Option A", "Option B", "Option C"], cf.choices)
171
176
 
172
177
  # Assign a value to the first Location
173
178
  location = Location.objects.get(name="Location A")
@@ -199,9 +204,14 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
199
204
  cf.save()
200
205
  cf.content_types.set([obj_type])
201
206
 
202
- CustomFieldChoice.objects.create(custom_field=cf, value="Option A")
203
- CustomFieldChoice.objects.create(custom_field=cf, value="Option B")
204
- CustomFieldChoice.objects.create(custom_field=cf, value="Option C")
207
+ CustomFieldChoice.objects.create(custom_field=cf, value="Option A", weight=100)
208
+ self.assertEqual(["Option A"], cf.choices)
209
+ CustomFieldChoice.objects.create(custom_field=cf, value="Option B", weight=200)
210
+ self.assertEqual(["Option A", "Option B"], cf.choices)
211
+ CustomFieldChoice.objects.create(custom_field=cf, value="Option C", weight=300)
212
+ self.assertEqual(["Option A", "Option B", "Option C"], cf.choices)
213
+ with self.assertNumQueries(0): # verify caching
214
+ self.assertEqual(["Option A", "Option B", "Option C"], cf.choices)
205
215
 
206
216
  # Assign a value to the first Location
207
217
  location = Location.objects.get(name="Location A")
@@ -293,21 +303,18 @@ class CustomFieldTest(ModelTestCases.BaseModelTestCase, TestCase):
293
303
  # Assign a disallowed value (list) to the first Location
294
304
  location = Location.objects.get(name="Location A")
295
305
  location.cf[cf.key] = ["I", "am", "a", "list"]
296
- with self.assertRaises(ValidationError) as context:
306
+ with self.assertRaisesRegex(ValidationError, "Value must be a string"):
297
307
  location.validated_save()
298
- self.assertIn("Value must be a string", str(context.exception))
299
308
 
300
309
  # Assign another disallowed value (int) to the first Location
301
310
  location.cf[cf.key] = 2
302
- with self.assertRaises(ValidationError) as context:
311
+ with self.assertRaisesRegex(ValidationError, "Value must be a string"):
303
312
  location.validated_save()
304
- self.assertIn("Value must be a string", str(context.exception))
305
313
 
306
314
  # Assign another disallowed value (bool) to the first Location
307
315
  location.cf[cf.key] = True
308
- with self.assertRaises(ValidationError) as context:
316
+ with self.assertRaisesRegex(ValidationError, "Value must be a string"):
309
317
  location.validated_save()
310
- self.assertIn("Value must be a string", str(context.exception))
311
318
 
312
319
  # Delete the stored value
313
320
  location.cf.pop(cf.key)
@@ -1294,7 +1301,7 @@ class CustomFieldModelTest(TestCase):
1294
1301
  custom_field.content_types.set([ContentType.objects.get_for_model(Provider)])
1295
1302
 
1296
1303
  provider = Provider.objects.create(name="Test")
1297
- with self.assertRaises(ValidationError):
1304
+ with self.assertRaisesRegex(ValidationError, "Missing required custom field 'custom_field'"):
1298
1305
  provider.validated_save()
1299
1306
 
1300
1307
  def test_custom_field_required_on_update(self):
@@ -1312,7 +1319,7 @@ class CustomFieldModelTest(TestCase):
1312
1319
  provider = Provider.objects.create(name="Test", _custom_field_data={"custom_field": "Value"})
1313
1320
  provider.validated_save()
1314
1321
  provider._custom_field_data.pop("custom_field")
1315
- with self.assertRaises(ValidationError):
1322
+ with self.assertRaisesRegex(ValidationError, "Missing required custom field 'custom_field'"):
1316
1323
  provider.validated_save()
1317
1324
 
1318
1325
  def test_update_removed_custom_field(self):
@@ -1373,7 +1380,7 @@ class CustomFieldModelTest(TestCase):
1373
1380
  """
1374
1381
  Check that a ValidationError is raised if any required custom fields are not present.
1375
1382
  """
1376
- cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, label="Baz", required=True)
1383
+ cf3 = CustomField(key="baz", type=CustomFieldTypeChoices.TYPE_TEXT, label="Baz", required=True)
1377
1384
  cf3.save()
1378
1385
  cf3.content_types.set([ContentType.objects.get_for_model(Location)])
1379
1386
 
@@ -1381,7 +1388,7 @@ class CustomFieldModelTest(TestCase):
1381
1388
 
1382
1389
  # Set custom field data with a required field omitted
1383
1390
  location.cf["foo"] = "abc"
1384
- with self.assertRaises(ValidationError):
1391
+ with self.assertRaisesRegex(ValidationError, "Missing required custom field 'baz'"):
1385
1392
  location.clean()
1386
1393
 
1387
1394
  location.cf["baz"] = "def"
@@ -1427,38 +1434,20 @@ class CustomFieldModelTest(TestCase):
1427
1434
  """
1428
1435
  Check the GraphQL validation method on CustomField Key Attribute.
1429
1436
  """
1430
- # Check if it catches the cf.key starting with a digit.
1431
- cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, label="Test 1", key="12_test_1")
1432
- with self.assertRaises(ValidationError) as error:
1433
- cf1.validated_save()
1434
- self.assertIn(
1435
- "This key is not Python/GraphQL safe. Please do not start the key with a digit and do not use hyphens or whitespace",
1436
- str(error.exception),
1437
- )
1438
- # Check if it catches the cf.key with whitespace.
1439
- cf1.key = "test 1"
1440
- with self.assertRaises(ValidationError) as error:
1441
- cf1.validated_save()
1442
- self.assertIn(
1443
- "This key is not Python/GraphQL safe. Please do not start the key with a digit and do not use hyphens or whitespace",
1444
- str(error.exception),
1445
- )
1446
- # Check if it catches the cf.key with hyphens.
1447
- cf1.key = "test-1-custom-field"
1448
- with self.assertRaises(ValidationError) as error:
1449
- cf1.validated_save()
1450
- self.assertIn(
1451
- "This key is not Python/GraphQL safe. Please do not start the key with a digit and do not use hyphens or whitespace",
1452
- str(error.exception),
1453
- )
1454
- # Check if it catches the cf.key with special characters
1455
- cf1.key = "test_1_custom_f)(&d"
1456
- with self.assertRaises(ValidationError) as error:
1457
- cf1.validated_save()
1458
- self.assertIn(
1459
- "This key is not Python/GraphQL safe. Please do not start the key with a digit and do not use hyphens or whitespace",
1460
- str(error.exception),
1461
- )
1437
+ cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, label="Test 1")
1438
+ for invalid_key in [
1439
+ "12_test_1", # Check if it catches the cf.key starting with a digit.
1440
+ "test 1", # Check if it catches the cf.key with whitespace.
1441
+ "test-1-custom-field", # Check if it catches the cf.key with hyphens.
1442
+ "test_1_custom_f)(&d", # Check if it catches the cf.key with special characters
1443
+ ]:
1444
+ with self.assertRaisesRegex(
1445
+ ValidationError,
1446
+ "This key is not Python/GraphQL safe. "
1447
+ "Please do not start the key with a digit and do not use hyphens or whitespace",
1448
+ ):
1449
+ cf1.key = invalid_key
1450
+ cf1.validated_save()
1462
1451
 
1463
1452
 
1464
1453
  class CustomFieldFilterTest(TestCase):
@@ -1934,9 +1923,11 @@ class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
1934
1923
  )
1935
1924
  self.cf.save()
1936
1925
  self.cf.content_types.set([obj_type])
1926
+ self.assertEqual(self.cf.choices, [])
1937
1927
 
1938
1928
  self.choice = CustomFieldChoice(custom_field=self.cf, value="Foo")
1939
1929
  self.choice.save()
1930
+ self.assertEqual(self.cf.choices, ["Foo"])
1940
1931
 
1941
1932
  location_status = Status.objects.get_for_model(Location).first()
1942
1933
  self.location_type = LocationType.objects.get(name="Campus")
@@ -1952,7 +1943,7 @@ class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
1952
1943
 
1953
1944
  def test_default_value_must_be_valid_choice_sad_path(self):
1954
1945
  self.cf.default = "invalid value"
1955
- with self.assertRaises(ValidationError):
1946
+ with self.assertRaisesRegex(ValidationError, 'Invalid default value "invalid value"'):
1956
1947
  self.cf.full_clean()
1957
1948
 
1958
1949
  def test_default_value_must_be_valid_choice_happy_path(self):
@@ -1965,6 +1956,13 @@ class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
1965
1956
  with self.assertRaises(ProtectedError):
1966
1957
  self.choice.delete()
1967
1958
 
1959
+ def test_inactive_choice_can_be_deleted(self):
1960
+ self.location._custom_field_data.pop("cf1")
1961
+ self.location.validated_save()
1962
+ self.assertEqual(self.cf.choices, ["Foo"])
1963
+ self.choice.delete()
1964
+ self.assertEqual(self.cf.choices, [])
1965
+
1968
1966
  def test_custom_choice_deleted_with_field(self):
1969
1967
  self.cf.delete()
1970
1968
  self.assertEqual(CustomField.objects.count(), 1) # custom field automatically added by the Example App
@@ -30,6 +30,7 @@ from nautobot.extras.models import (
30
30
  ConfigContextSchema,
31
31
  ExportTemplate,
32
32
  GitRepository,
33
+ GraphQLQuery,
33
34
  Job,
34
35
  JobButton,
35
36
  JobHook,
@@ -197,6 +198,15 @@ class GitTest(TransactionTestCase):
197
198
  )
198
199
  self.assertIsNotNone(export_template_vlan)
199
200
 
201
+ def assert_graphql_query_exists(self, name="device_names.gql"):
202
+ """Helper function to assert Graphql query exists."""
203
+ graphql_query = GraphQLQuery.objects.get(
204
+ name=name,
205
+ owner_object_id=self.repo.pk,
206
+ owner_content_type=ContentType.objects.get_for_model(GitRepository),
207
+ )
208
+ self.assertIsNotNone(graphql_query)
209
+
200
210
  def assert_job_exists(self, name="MyJob", installed=True):
201
211
  """Helper function to assert JobModel and registered Job exist."""
202
212
  # Is it registered correctly in the database?
@@ -348,6 +358,10 @@ class GitTest(TransactionTestCase):
348
358
  # Case when ContentType.model != ContentType.name, template was added and deleted during sync (#570)
349
359
  self.assert_export_template_vlan_exists("template.j2")
350
360
 
361
+ # Make sure Graphgl queries were loaded
362
+ self.assert_graphql_query_exists("device_names")
363
+ self.assert_graphql_query_exists("device_interfaces")
364
+
351
365
  # Make sure Jobs were successfully loaded from file and registered as JobModels
352
366
  self.assert_job_exists(name="MyJob")
353
367
  self.assert_job_exists(name="MyJobButtonReceiver")
@@ -439,6 +453,7 @@ class GitTest(TransactionTestCase):
439
453
  self.assertFalse(ConfigContextSchema.objects.filter(owner_object_id=self.repo.id).exists())
440
454
  self.assertFalse(ConfigContext.objects.filter(owner_object_id=self.repo.id).exists())
441
455
  self.assertFalse(ExportTemplate.objects.filter(owner_object_id=self.repo.id).exists())
456
+ self.assertFalse(GraphQLQuery.objects.filter(owner_object_id=self.repo.id).exists())
442
457
  self.assertFalse(Job.objects.filter(module_name__startswith=f"{self.repo.slug}.").exists())
443
458
  device = Device.objects.get(name=self.device.name)
444
459
  self.assertIsNone(device.local_config_context_data)
@@ -505,6 +520,11 @@ class GitTest(TransactionTestCase):
505
520
  grouping="jobs",
506
521
  message__contains="Error in loading Jobs from Git repository: ",
507
522
  )
523
+ failure_logs.get(
524
+ grouping="graphql queries",
525
+ message__contains="Error processing GraphQL query file 'bad_device_names.gql': Syntax Error GraphQL (4:5) Expected Name, found }",
526
+ )
527
+
508
528
  except (AssertionError, JobLogEntry.DoesNotExist):
509
529
  for log in log_entries:
510
530
  print(log.message)
@@ -630,6 +650,8 @@ class GitTest(TransactionTestCase):
630
650
  self.assert_export_template_device("template.j2")
631
651
  self.assert_export_template_html_exist("template2.html")
632
652
  self.assert_export_template_vlan_exists("template.j2")
653
+ self.assert_graphql_query_exists(name="device_names")
654
+ self.assert_graphql_query_exists(name="device_interfaces")
633
655
  self.assert_job_exists(name="MyJob")
634
656
  self.assert_job_exists(name="MyJobButtonReceiver")
635
657
  self.assert_job_exists(name="MyJobHookReceiver")
@@ -669,6 +691,8 @@ class GitTest(TransactionTestCase):
669
691
  self.assert_export_template_device("template.j2")
670
692
  self.assert_export_template_html_exist("template2.html")
671
693
  self.assert_export_template_vlan_exists("template.j2")
694
+ self.assert_graphql_query_exists("device_names")
695
+ self.assert_graphql_query_exists("device_interfaces")
672
696
  self.assert_job_exists(name="MyJob")
673
697
  self.assert_job_exists(name="MyJobButtonReceiver")
674
698
  self.assert_job_exists(name="MyJobHookReceiver")
@@ -711,6 +735,8 @@ class GitTest(TransactionTestCase):
711
735
  log_entries.get(message__contains="Addition - `export_templates/dcim/device/template.j2`")
712
736
  log_entries.get(message__contains="Addition - `export_templates/dcim/device/template2.html`")
713
737
  log_entries.get(message__contains="Addition - `export_templates/ipam/vlan/template.j2`")
738
+ log_entries.get(message__contains="Addition - `graphql_queries/device_interfaces.gql`")
739
+ log_entries.get(message__contains="Addition - `graphql_queries/device_names.gql`")
714
740
  log_entries.get(message__contains="Addition - `jobs/__init__.py`")
715
741
  log_entries.get(message__contains="Addition - `jobs/my_job.py`")
716
742
  except JobLogEntry.DoesNotExist:
@@ -721,6 +747,7 @@ class GitTest(TransactionTestCase):
721
747
  self.assertFalse(ConfigContextSchema.objects.filter(owner_object_id=self.repo.pk).exists())
722
748
  self.assertFalse(ConfigContext.objects.filter(owner_object_id=self.repo.pk).exists())
723
749
  self.assertFalse(ExportTemplate.objects.filter(owner_object_id=self.repo.pk).exists())
750
+ self.assertFalse(GraphQLQuery.objects.filter(owner_object_id=self.repo.pk).exists())
724
751
  self.assertFalse(Job.objects.filter(module_name__startswith=self.repo.slug).exists())
725
752
 
726
753
  # TODO: test dry-run against a branch name
@@ -1,5 +1,6 @@
1
1
  from datetime import datetime, timedelta, timezone
2
2
  import os
3
+ import shutil
3
4
  import tempfile
4
5
  from unittest import expectedFailure, mock
5
6
  import uuid
@@ -17,6 +18,7 @@ from django.test import override_settings
17
18
  from django.test.utils import isolate_apps
18
19
  from django.utils.timezone import get_default_timezone, now
19
20
  from django_celery_beat.tzcrontab import TzAwareCrontab
21
+ from git import GitCommandError
20
22
  from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
21
23
  import time_machine
22
24
 
@@ -48,6 +50,7 @@ from nautobot.extras.constants import (
48
50
  JOB_LOG_MAX_LOG_OBJECT_LENGTH,
49
51
  JOB_OVERRIDABLE_FIELDS,
50
52
  )
53
+ from nautobot.extras.datasources.registry import get_datasource_contents
51
54
  from nautobot.extras.jobs import get_job
52
55
  from nautobot.extras.models import (
53
56
  ComputedField,
@@ -83,6 +86,7 @@ from nautobot.extras.models import (
83
86
  from nautobot.extras.models.statuses import StatusModel
84
87
  from nautobot.extras.registry import registry
85
88
  from nautobot.extras.secrets.exceptions import SecretParametersError, SecretProviderError, SecretValueNotFoundError
89
+ from nautobot.extras.tests.git_helper import create_and_populate_git_repository
86
90
  from nautobot.ipam.models import IPAddress
87
91
  from nautobot.tenancy.models import Tenant
88
92
  from nautobot.virtualization.models import (
@@ -1072,6 +1076,285 @@ class GitRepositoryTest(ModelTestCases.BaseModelTestCase):
1072
1076
  self.repo.remote_url = "http://some-private-host/example.git"
1073
1077
  self.repo.validated_save()
1074
1078
 
1079
+ def test_clone_to_directory_context_manager(self):
1080
+ """Confirm that the clone_to_directory_context() context manager method works as expected."""
1081
+ try:
1082
+ specified_path = tempfile.mkdtemp()
1083
+ self.tempdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
1084
+ create_and_populate_git_repository(self.tempdir.name, divergent_branch="divergent-branch")
1085
+ self.repo_slug = "new_git_repo"
1086
+ self.repo = GitRepository(
1087
+ name="New Git Repository",
1088
+ slug=self.repo_slug,
1089
+ remote_url="file://"
1090
+ + self.tempdir.name, # file:// URLs aren't permitted normally, but very useful here!
1091
+ branch="main",
1092
+ # Provide everything we know we can provide
1093
+ provided_contents=[
1094
+ entry.content_identifier for entry in get_datasource_contents("extras.gitrepository")
1095
+ ],
1096
+ )
1097
+ self.repo.save()
1098
+ with self.subTest("Clone a repository with no path argument provided"):
1099
+ with self.repo.clone_to_directory_context() as path:
1100
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1101
+ self.assertTrue(path.startswith(tempfile.gettempdir()))
1102
+ self.assertTrue(os.path.exists(path))
1103
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1104
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema2.json"))
1105
+ self.assertFalse(os.path.exists(path))
1106
+
1107
+ with self.subTest("Clone a repository with a path argument provided"):
1108
+ with self.repo.clone_to_directory_context(path=specified_path) as path:
1109
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1110
+ self.assertTrue(path.startswith(specified_path))
1111
+ self.assertTrue(os.path.exists(path))
1112
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1113
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema2.json"))
1114
+ # Temp directory is cleaned up after the context manager exits
1115
+ self.assertFalse(os.path.exists(path))
1116
+
1117
+ with self.subTest("Clone a repository with the branch argument provided"):
1118
+ with self.repo.clone_to_directory_context(path=specified_path, branch="main") as path:
1119
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1120
+ self.assertTrue(path.startswith(specified_path))
1121
+ self.assertTrue(os.path.exists(path))
1122
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1123
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema2.json"))
1124
+ # Temp directory is cleaned up after the context manager exits
1125
+ self.assertFalse(os.path.exists(path))
1126
+
1127
+ with self.subTest("Clone a repository with non-default branch provided"):
1128
+ with self.repo.clone_to_directory_context(path=specified_path, branch="empty-repo") as path:
1129
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1130
+ self.assertTrue(path.startswith(specified_path))
1131
+ self.assertTrue(os.path.exists(path))
1132
+ # empty-repo should contain no files
1133
+ self.assertFalse(os.path.exists(path + "/config_context_schemas"))
1134
+ self.assertFalse(os.path.exists(path + "/config_contexts"))
1135
+ # Temp directory is cleaned up after the context manager exits
1136
+ self.assertFalse(os.path.exists(path))
1137
+
1138
+ with self.subTest("Clone a repository with divergent branch provided"):
1139
+ with self.repo.clone_to_directory_context(path=specified_path, branch="divergent-branch") as path:
1140
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1141
+ self.assertTrue(path.startswith(specified_path))
1142
+ self.assertTrue(os.path.exists(path))
1143
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1144
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema2.json"))
1145
+ # Temp directory is cleaned up after the context manager exits
1146
+ self.assertFalse(os.path.exists(path))
1147
+
1148
+ with self.subTest("Clone a repository with the head argument provided"):
1149
+ with self.repo.clone_to_directory_context(path=specified_path, head="valid-files") as path:
1150
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1151
+ self.assertTrue(path.startswith(specified_path))
1152
+ self.assertTrue(os.path.exists(path))
1153
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/schema-1.yaml"))
1154
+ self.assertTrue(os.path.exists(path + "/config_contexts/context.yaml"))
1155
+ # Temp directory is cleaned up after the context manager exits
1156
+ self.assertFalse(os.path.exists(path))
1157
+
1158
+ with self.subTest("Clone a repository with depth argument provided"):
1159
+ with self.repo.clone_to_directory_context(path=specified_path, depth=1) as path:
1160
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1161
+ self.assertTrue(path.startswith(specified_path))
1162
+ self.assertTrue(os.path.exists(path))
1163
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1164
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1165
+ # Temp directory is cleaned up after the context manager exits
1166
+ self.assertFalse(os.path.exists(path))
1167
+
1168
+ with self.subTest("Clone a shallow repository with depth and valid head arguments provided"):
1169
+ with self.repo.clone_to_directory_context(
1170
+ path=specified_path, depth=1, head="divergent-branch-tag"
1171
+ ) as path:
1172
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1173
+ self.assertTrue(path.startswith(specified_path))
1174
+ self.assertTrue(os.path.exists(path))
1175
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1176
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1177
+ # Temp directory is cleaned up after the context manager exits
1178
+ self.assertFalse(os.path.exists(path))
1179
+
1180
+ with self.subTest("Clone a shallow repository with depth and invalid head arguments provided"):
1181
+ with self.assertRaisesRegex(GitCommandError, "malformed object name valid-files"):
1182
+ # Shallow copy a repo should only have the latest commit
1183
+ with self.repo.clone_to_directory_context(path=specified_path, depth=1, head="valid-files") as path:
1184
+ pass
1185
+
1186
+ with self.subTest("Clone a shallow repository with depth and valid branch arguments provided"):
1187
+ with self.repo.clone_to_directory_context(path=specified_path, depth=1, branch="main") as path:
1188
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1189
+ self.assertTrue(path.startswith(specified_path))
1190
+ self.assertTrue(os.path.exists(path))
1191
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1192
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1193
+ # Temp directory is cleaned up after the context manager exits
1194
+ self.assertFalse(os.path.exists(path))
1195
+
1196
+ with self.subTest("Clone a shallow repository with depth and divergent branch arguments provided"):
1197
+ with self.repo.clone_to_directory_context(
1198
+ path=specified_path, depth=1, branch="divergent-branch"
1199
+ ) as path:
1200
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1201
+ self.assertTrue(path.startswith(specified_path))
1202
+ self.assertTrue(os.path.exists(path))
1203
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1204
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1205
+ # Temp directory is cleaned up after the context manager exits
1206
+ self.assertFalse(os.path.exists(path))
1207
+
1208
+ with self.subTest("Assert a GitCommandError is raised when an invalid commit hash is provided"):
1209
+ with self.assertRaisesRegex(GitCommandError, "malformed object name non-existent"):
1210
+ with self.repo.clone_to_directory_context(path=specified_path, head="non-existent") as path:
1211
+ pass
1212
+
1213
+ with self.subTest("Assert a value error is raised when branch and head are both provided"):
1214
+ with self.assertRaisesRegex(ValueError, "Cannot specify both branch and head"):
1215
+ with self.repo.clone_to_directory_context(branch="main", head="valid-files") as path:
1216
+ pass
1217
+ finally:
1218
+ shutil.rmtree(specified_path, ignore_errors=True)
1219
+ shutil.rmtree(self.tempdir.name, ignore_errors=True)
1220
+
1221
+ def test_clone_to_directory_helper_methods(self):
1222
+ """Confirm that the clone_to_directory()/cleanup_cloned_directory() methods work as expected."""
1223
+ try:
1224
+ specified_path = tempfile.mkdtemp()
1225
+ self.tempdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
1226
+ create_and_populate_git_repository(self.tempdir.name, divergent_branch="divergent-branch")
1227
+ self.repo_slug = "new_git_repo"
1228
+ self.repo = GitRepository(
1229
+ name="New Git Repository",
1230
+ slug=self.repo_slug,
1231
+ remote_url="file://"
1232
+ + self.tempdir.name, # file:// URLs aren't permitted normally, but very useful here!
1233
+ branch="main",
1234
+ # Provide everything we know we can provide
1235
+ provided_contents=[
1236
+ entry.content_identifier for entry in get_datasource_contents("extras.gitrepository")
1237
+ ],
1238
+ )
1239
+ self.repo.save()
1240
+ with self.subTest("Clone a repository with no path argument provided"):
1241
+ path = self.repo.clone_to_directory()
1242
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1243
+ self.assertTrue(path.startswith(tempfile.gettempdir()))
1244
+ self.assertTrue(os.path.exists(path))
1245
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1246
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema2.json"))
1247
+ self.repo.cleanup_cloned_directory(path)
1248
+ self.assertFalse(os.path.exists(path))
1249
+
1250
+ with self.subTest("Clone a repository with a path argument provided"):
1251
+ path = self.repo.clone_to_directory(path=specified_path)
1252
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1253
+ self.assertTrue(path.startswith(specified_path))
1254
+ self.assertTrue(os.path.exists(path))
1255
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1256
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema2.json"))
1257
+ self.repo.cleanup_cloned_directory(path)
1258
+ self.assertFalse(os.path.exists(path))
1259
+
1260
+ with self.subTest("Clone a repository with the branch argument provided"):
1261
+ path = self.repo.clone_to_directory(path=specified_path, branch="main")
1262
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1263
+ self.assertTrue(path.startswith(specified_path))
1264
+ self.assertTrue(os.path.exists(path))
1265
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1266
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema2.json"))
1267
+ self.repo.cleanup_cloned_directory(path)
1268
+ self.assertFalse(os.path.exists(path))
1269
+
1270
+ with self.subTest("Clone a repository with non-default branch provided"):
1271
+ path = self.repo.clone_to_directory(path=specified_path, branch="empty-repo")
1272
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1273
+ self.assertTrue(path.startswith(specified_path))
1274
+ self.assertTrue(os.path.exists(path))
1275
+ self.assertFalse(os.path.exists(path + "/config_context_schemas"))
1276
+ self.assertFalse(os.path.exists(path + "/config_contexts"))
1277
+ self.repo.cleanup_cloned_directory(path)
1278
+ self.assertFalse(os.path.exists(path))
1279
+
1280
+ with self.subTest("Clone a repository with divergent branch provided"):
1281
+ path = self.repo.clone_to_directory(path=specified_path, branch="divergent-branch")
1282
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1283
+ self.assertTrue(path.startswith(specified_path))
1284
+ self.assertTrue(os.path.exists(path))
1285
+ self.assertTrue(os.path.exists(path + "/config_context_schemas"))
1286
+ self.assertTrue(os.path.exists(path + "/config_contexts"))
1287
+ self.repo.cleanup_cloned_directory(path)
1288
+ self.assertFalse(os.path.exists(path))
1289
+
1290
+ with self.subTest("Clone a repository with the head argument provided"):
1291
+ path = self.repo.clone_to_directory(path=specified_path, head="valid-files")
1292
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1293
+ self.assertTrue(path.startswith(specified_path))
1294
+ self.assertTrue(os.path.exists(path))
1295
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/schema-1.yaml"))
1296
+ self.assertTrue(os.path.exists(path + "/config_contexts/context.yaml"))
1297
+ self.repo.cleanup_cloned_directory(path)
1298
+ self.assertFalse(os.path.exists(path))
1299
+
1300
+ with self.subTest("Clone a repository with depth argument provided"):
1301
+ path = self.repo.clone_to_directory(path=specified_path, depth=1)
1302
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1303
+ self.assertTrue(path.startswith(specified_path))
1304
+ self.assertTrue(os.path.exists(path))
1305
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1306
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1307
+ self.repo.cleanup_cloned_directory(path)
1308
+ self.assertFalse(os.path.exists(path))
1309
+
1310
+ with self.subTest("Clone a shallow repository with depth and valid head arguments provided"):
1311
+ path = self.repo.clone_to_directory(path=specified_path, depth=1, head="divergent-branch-tag")
1312
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1313
+ self.assertTrue(path.startswith(specified_path))
1314
+ self.assertTrue(os.path.exists(path))
1315
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1316
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1317
+ self.repo.cleanup_cloned_directory(path)
1318
+ self.assertFalse(os.path.exists(path))
1319
+
1320
+ with self.subTest("Clone a shallow repository with depth and invalid head arguments provided"):
1321
+ with self.assertRaisesRegex(GitCommandError, "malformed object name valid-files"):
1322
+ # Shallow copy a repo should only have the latest commit
1323
+ path = self.repo.clone_to_directory(path=specified_path, depth=1, head="valid-files")
1324
+ self.repo.cleanup_cloned_directory(path)
1325
+ self.assertFalse(os.path.exists(path))
1326
+
1327
+ with self.subTest("Clone a shallow repository with depth and valid branch arguments provided"):
1328
+ path = self.repo.clone_to_directory(path=specified_path, depth=1, branch="main")
1329
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1330
+ self.assertTrue(path.startswith(specified_path))
1331
+ self.assertTrue(os.path.exists(path))
1332
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1333
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1334
+ self.repo.cleanup_cloned_directory(path)
1335
+ self.assertFalse(os.path.exists(path))
1336
+
1337
+ with self.subTest("Clone a shallow repository with depth and divergent branch arguments provided"):
1338
+ path = self.repo.clone_to_directory(path=specified_path, depth=1, branch="divergent-branch")
1339
+ # assert that the temporary directory was created in the expected location i.e. /tmp/
1340
+ self.assertTrue(path.startswith(specified_path))
1341
+ self.assertTrue(os.path.exists(path))
1342
+ self.assertTrue(os.path.exists(path + "/config_context_schemas/badschema1.json"))
1343
+ self.assertTrue(os.path.exists(path + "/config_contexts/badcontext2.json"))
1344
+ self.repo.cleanup_cloned_directory(path)
1345
+ self.assertFalse(os.path.exists(path))
1346
+
1347
+ with self.subTest("Assert a GitCommandError is raised when an invalid commit hash is provided"):
1348
+ with self.assertRaisesRegex(GitCommandError, "malformed object name non-existent"):
1349
+ path = self.repo.clone_to_directory(path=specified_path, head="non-existent")
1350
+
1351
+ with self.subTest("Assert a ValuError is raised when branch and head are both provided"):
1352
+ with self.assertRaisesRegex(ValueError, "Cannot specify both branch and head"):
1353
+ path = self.repo.clone_to_directory(branch="main", head="valid-files")
1354
+ finally:
1355
+ shutil.rmtree(specified_path, ignore_errors=True)
1356
+ shutil.rmtree(self.tempdir.name, ignore_errors=True)
1357
+
1075
1358
 
1076
1359
  class JobModelTest(ModelTestCases.BaseModelTestCase):
1077
1360
  """