nautobot 2.4.1__py3-none-any.whl → 2.4.3__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 (461) hide show
  1. nautobot/circuits/templates/circuits/inc/circuit_termination.html +1 -1
  2. nautobot/circuits/tests/integration/test_circuit.py +135 -0
  3. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +43 -0
  4. nautobot/circuits/tests/integration/test_relationships.py +1 -1
  5. nautobot/circuits/views.py +4 -1
  6. nautobot/cloud/api/views.py +3 -3
  7. nautobot/core/apps/__init__.py +0 -5
  8. nautobot/core/constants.py +0 -1
  9. nautobot/core/forms/__init__.py +2 -0
  10. nautobot/core/forms/forms.py +2 -1
  11. nautobot/core/forms/widgets.py +8 -0
  12. nautobot/core/management/commands/generate_performance_test_endpoints.py +268 -0
  13. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  14. nautobot/core/templates/generic/object_bulk_destroy.html +1 -1
  15. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  16. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  17. nautobot/core/templates/generic/object_create.html +5 -0
  18. nautobot/core/templates/generic/object_delete.html +1 -1
  19. nautobot/core/templates/generic/object_detail.html +1 -1
  20. nautobot/core/templates/generic/object_edit.html +1 -1
  21. nautobot/core/templates/inc/javascript.html +2 -0
  22. nautobot/core/templates/widgets/clearable_file.html +5 -0
  23. nautobot/core/templatetags/helpers.py +3 -3
  24. nautobot/core/testing/integration.py +469 -12
  25. nautobot/core/tests/test_commands.py +31 -0
  26. nautobot/core/tests/test_jobs.py +34 -2
  27. nautobot/core/tests/test_utils.py +17 -2
  28. nautobot/core/utils/git.py +7 -2
  29. nautobot/core/utils/lookup.py +12 -1
  30. nautobot/core/views/generic.py +10 -2
  31. nautobot/core/views/mixins.py +22 -7
  32. nautobot/core/views/utils.py +2 -2
  33. nautobot/dcim/api/views.py +11 -10
  34. nautobot/dcim/forms.py +15 -6
  35. nautobot/dcim/models/devices.py +1 -2
  36. nautobot/dcim/tables/devices.py +2 -1
  37. nautobot/dcim/templates/dcim/cable.html +1 -1
  38. nautobot/dcim/templates/dcim/cable_trace.html +4 -4
  39. nautobot/dcim/templates/dcim/consoleport.html +14 -4
  40. nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
  41. nautobot/dcim/templates/dcim/device/base.html +1 -1
  42. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
  43. nautobot/dcim/templates/dcim/device.html +2 -2
  44. nautobot/dcim/templates/dcim/device_component.html +1 -1
  45. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  46. nautobot/dcim/templates/dcim/frontport.html +7 -2
  47. nautobot/dcim/templates/dcim/interface.html +9 -4
  48. nautobot/dcim/templates/dcim/location.html +1 -1
  49. nautobot/dcim/templates/dcim/locationtype.html +1 -1
  50. nautobot/dcim/templates/dcim/locationtype_retrieve.html +1 -1
  51. nautobot/dcim/templates/dcim/manufacturer.html +1 -1
  52. nautobot/dcim/templates/dcim/platform.html +1 -1
  53. nautobot/dcim/templates/dcim/powerfeed.html +9 -4
  54. nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
  55. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  56. nautobot/dcim/templates/dcim/powerport.html +14 -4
  57. nautobot/dcim/templates/dcim/rack.html +1 -1
  58. nautobot/dcim/templates/dcim/rackgroup.html +1 -1
  59. nautobot/dcim/templates/dcim/rackreservation.html +2 -2
  60. nautobot/dcim/templates/dcim/rearport.html +7 -2
  61. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  62. nautobot/dcim/tests/integration/test_device_bulk_operations.py +30 -0
  63. nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
  64. nautobot/dcim/tests/integration/test_location_bulk_operations.py +43 -0
  65. nautobot/dcim/tests/test_models.py +1 -1
  66. nautobot/dcim/tests/test_views.py +9 -1
  67. nautobot/dcim/views.py +12 -15
  68. nautobot/extras/api/serializers.py +33 -0
  69. nautobot/extras/api/views.py +13 -5
  70. nautobot/extras/constants.py +1 -0
  71. nautobot/extras/datasources/git.py +125 -0
  72. nautobot/extras/forms/forms.py +4 -0
  73. nautobot/extras/jobs.py +8 -1
  74. nautobot/extras/migrations/0122_add_graphqlquery_owner_content_type.py +34 -0
  75. nautobot/extras/models/customfields.py +29 -12
  76. nautobot/extras/models/datasources.py +85 -0
  77. nautobot/extras/models/models.py +15 -0
  78. nautobot/extras/models/relationships.py +17 -5
  79. nautobot/extras/signals.py +15 -1
  80. nautobot/extras/templates/extras/computedfield.html +1 -1
  81. nautobot/extras/templates/extras/configcontext.html +1 -1
  82. nautobot/extras/templates/extras/configcontextschema.html +1 -1
  83. nautobot/extras/templates/extras/customfield.html +1 -1
  84. nautobot/extras/templates/extras/customlink.html +1 -1
  85. nautobot/extras/templates/extras/dynamicgroup.html +1 -1
  86. nautobot/extras/templates/extras/exporttemplate.html +1 -1
  87. nautobot/extras/templates/extras/gitrepository.html +1 -1
  88. nautobot/extras/templates/extras/graphqlquery.html +1 -1
  89. nautobot/extras/templates/extras/job.html +1 -0
  90. nautobot/extras/templates/extras/job_detail.html +1 -1
  91. nautobot/extras/templates/extras/jobbutton_retrieve.html +1 -1
  92. nautobot/extras/templates/extras/jobhook.html +1 -1
  93. nautobot/extras/templates/extras/jobresult.html +1 -1
  94. nautobot/extras/templates/extras/objectchange.html +1 -1
  95. nautobot/extras/templates/extras/plugin_detail.html +1 -1
  96. nautobot/extras/templates/extras/relationship.html +1 -63
  97. nautobot/extras/templates/extras/role_retrieve.html +1 -1
  98. nautobot/extras/templates/extras/scheduledjob.html +1 -1
  99. nautobot/extras/templates/extras/secret.html +1 -1
  100. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  101. nautobot/extras/templates/extras/status.html +1 -1
  102. nautobot/extras/templates/extras/tag.html +1 -1
  103. nautobot/extras/templates/extras/webhook.html +1 -1
  104. nautobot/extras/tests/git_data/01-valid-files/graphql_queries/device_interfaces.gql +8 -0
  105. nautobot/extras/tests/git_data/01-valid-files/graphql_queries/device_names.gql +5 -0
  106. nautobot/extras/tests/git_data/02-invalid-files/graphql_queries/bad_device_names.gql +5 -0
  107. nautobot/extras/tests/git_helper.py +9 -1
  108. nautobot/extras/tests/integration/__init__.py +29 -16
  109. nautobot/extras/tests/test_api.py +6 -0
  110. nautobot/extras/tests/test_customfields.py +49 -51
  111. nautobot/extras/tests/test_datasources.py +27 -0
  112. nautobot/extras/tests/test_dynamicgroups.py +14 -0
  113. nautobot/extras/tests/test_models.py +283 -0
  114. nautobot/extras/tests/test_utils.py +22 -1
  115. nautobot/extras/tests/test_views.py +197 -9
  116. nautobot/extras/utils.py +47 -8
  117. nautobot/extras/views.py +84 -26
  118. nautobot/ipam/api/views.py +3 -3
  119. nautobot/ipam/forms.py +2 -6
  120. nautobot/ipam/models.py +8 -2
  121. nautobot/ipam/tables.py +2 -2
  122. nautobot/ipam/templates/ipam/ipaddress.html +1 -1
  123. nautobot/ipam/templates/ipam/prefix.html +1 -1
  124. nautobot/ipam/templates/ipam/rir.html +1 -1
  125. nautobot/ipam/templates/ipam/routetarget.html +1 -1
  126. nautobot/ipam/templates/ipam/service.html +1 -1
  127. nautobot/ipam/templates/ipam/vlan.html +1 -1
  128. nautobot/ipam/templates/ipam/vlangroup.html +1 -1
  129. nautobot/ipam/templates/ipam/vrf.html +1 -1
  130. nautobot/ipam/tests/test_models.py +24 -0
  131. nautobot/ipam/tests/test_utils.py +41 -2
  132. nautobot/ipam/utils/__init__.py +18 -11
  133. nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
  134. nautobot/project-static/docs/404.html +87 -12
  135. nautobot/project-static/docs/apps/index.html +88 -13
  136. nautobot/project-static/docs/apps/nautobot-apps.html +88 -13
  137. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js → bundle.60a45f97.min.js} +1 -1
  138. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js.map → bundle.60a45f97.min.js.map} +1 -1
  139. nautobot/project-static/docs/assets/javascripts/workers/{search.6ce7567c.min.js → search.f8cc74c7.min.js} +1 -1
  140. nautobot/project-static/docs/assets/javascripts/workers/{search.6ce7567c.min.js.map → search.f8cc74c7.min.js.map} +1 -1
  141. nautobot/project-static/docs/assets/stylesheets/{main.6f8fc17f.min.css → main.a40c8224.min.css} +1 -1
  142. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +87 -12
  143. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +87 -12
  144. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +87 -12
  145. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +87 -12
  146. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +87 -12
  147. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +87 -12
  148. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +87 -12
  149. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +87 -12
  150. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +87 -12
  151. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +87 -12
  152. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +87 -12
  153. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +87 -12
  154. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +87 -12
  155. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +87 -12
  156. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +87 -12
  157. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +87 -12
  158. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +87 -12
  159. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +87 -12
  160. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +87 -12
  161. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +87 -12
  162. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +87 -12
  163. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +87 -12
  164. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +177 -20
  165. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +114 -17
  166. nautobot/project-static/docs/development/apps/api/configuration-view.html +87 -12
  167. nautobot/project-static/docs/development/apps/api/database-backend-config.html +87 -12
  168. nautobot/project-static/docs/development/apps/api/models/django-admin.html +87 -12
  169. nautobot/project-static/docs/development/apps/api/models/global-search.html +87 -12
  170. nautobot/project-static/docs/development/apps/api/models/graphql.html +96 -21
  171. nautobot/project-static/docs/development/apps/api/models/index.html +87 -12
  172. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +87 -12
  173. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +87 -12
  174. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +89 -14
  175. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +87 -12
  176. nautobot/project-static/docs/development/apps/api/platform-features/index.html +87 -12
  177. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +87 -12
  178. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +87 -12
  179. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +87 -12
  180. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +87 -12
  181. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +87 -12
  182. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +87 -12
  183. nautobot/project-static/docs/development/apps/api/prometheus.html +87 -12
  184. nautobot/project-static/docs/development/apps/api/setup.html +88 -13
  185. nautobot/project-static/docs/development/apps/api/testing.html +87 -12
  186. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +87 -12
  187. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +87 -12
  188. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +87 -12
  189. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +87 -12
  190. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +87 -12
  191. nautobot/project-static/docs/development/apps/api/views/base-template.html +87 -12
  192. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +87 -12
  193. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +87 -12
  194. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +87 -12
  195. nautobot/project-static/docs/development/apps/api/views/index.html +87 -12
  196. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +87 -12
  197. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +87 -12
  198. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +87 -12
  199. nautobot/project-static/docs/development/apps/api/views/notes.html +87 -12
  200. nautobot/project-static/docs/development/apps/api/views/rest-api.html +87 -12
  201. nautobot/project-static/docs/development/apps/api/views/urls.html +87 -12
  202. nautobot/project-static/docs/development/apps/index.html +87 -12
  203. nautobot/project-static/docs/development/apps/migration/code-updates.html +93 -17
  204. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +89 -14
  205. nautobot/project-static/docs/development/apps/migration/from-v1.html +90 -15
  206. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +87 -12
  207. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +87 -12
  208. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +87 -12
  209. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +87 -12
  210. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +87 -12
  211. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +87 -12
  212. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +88 -13
  213. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +87 -12
  214. nautobot/project-static/docs/development/apps/porting-from-netbox.html +87 -12
  215. nautobot/project-static/docs/development/core/application-registry.html +87 -12
  216. nautobot/project-static/docs/development/core/best-practices.html +88 -13
  217. nautobot/project-static/docs/development/core/bootstrap-ui.html +88 -13
  218. nautobot/project-static/docs/development/core/caching.html +87 -12
  219. nautobot/project-static/docs/development/core/controllers.html +87 -12
  220. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +94 -19
  221. nautobot/project-static/docs/development/core/generic-views.html +87 -12
  222. nautobot/project-static/docs/development/core/getting-started.html +89 -14
  223. nautobot/project-static/docs/development/core/homepage.html +87 -12
  224. nautobot/project-static/docs/development/core/index.html +88 -13
  225. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +90 -15
  226. nautobot/project-static/docs/development/core/model-checklist.html +88 -13
  227. nautobot/project-static/docs/development/core/model-features.html +87 -12
  228. nautobot/project-static/docs/development/core/natural-keys.html +87 -12
  229. nautobot/project-static/docs/development/core/navigation-menu.html +88 -13
  230. nautobot/project-static/docs/development/core/release-checklist.html +88 -13
  231. nautobot/project-static/docs/development/core/role-internals.html +87 -12
  232. nautobot/project-static/docs/development/core/settings.html +88 -13
  233. nautobot/project-static/docs/development/core/style-guide.html +91 -16
  234. nautobot/project-static/docs/development/core/templates.html +88 -13
  235. nautobot/project-static/docs/development/core/testing.html +87 -12
  236. nautobot/project-static/docs/development/core/ui-component-framework.html +87 -12
  237. nautobot/project-static/docs/development/core/user-preferences.html +87 -12
  238. nautobot/project-static/docs/development/index.html +87 -12
  239. nautobot/project-static/docs/development/jobs/index.html +95 -13
  240. nautobot/project-static/docs/development/jobs/migration/from-v1.html +90 -14
  241. nautobot/project-static/docs/index.html +90 -14
  242. nautobot/project-static/docs/objects.inv +0 -0
  243. nautobot/project-static/docs/overview/application_stack.html +89 -14
  244. nautobot/project-static/docs/overview/design_philosophy.html +87 -12
  245. nautobot/project-static/docs/release-notes/index.html +87 -12
  246. nautobot/project-static/docs/release-notes/version-1.0.html +89 -14
  247. nautobot/project-static/docs/release-notes/version-1.1.html +89 -14
  248. nautobot/project-static/docs/release-notes/version-1.2.html +90 -15
  249. nautobot/project-static/docs/release-notes/version-1.3.html +88 -13
  250. nautobot/project-static/docs/release-notes/version-1.4.html +104 -29
  251. nautobot/project-static/docs/release-notes/version-1.5.html +95 -20
  252. nautobot/project-static/docs/release-notes/version-1.6.html +91 -16
  253. nautobot/project-static/docs/release-notes/version-2.0.html +97 -22
  254. nautobot/project-static/docs/release-notes/version-2.1.html +94 -19
  255. nautobot/project-static/docs/release-notes/version-2.2.html +88 -13
  256. nautobot/project-static/docs/release-notes/version-2.3.html +91 -16
  257. nautobot/project-static/docs/release-notes/version-2.4.html +465 -12
  258. nautobot/project-static/docs/requirements.txt +1 -1
  259. nautobot/project-static/docs/search/search_index.json +1 -1
  260. nautobot/project-static/docs/sitemap.xml +296 -288
  261. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  262. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +90 -15
  263. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +87 -12
  264. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +91 -16
  265. nautobot/project-static/docs/user-guide/administration/configuration/index.html +87 -12
  266. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +88 -13
  267. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +90 -15
  268. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +87 -12
  269. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +95 -20
  270. nautobot/project-static/docs/user-guide/administration/guides/docker.html +90 -15
  271. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +88 -13
  272. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +87 -12
  273. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +91 -16
  274. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +87 -12
  275. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +102 -27
  276. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +89 -14
  277. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +87 -12
  278. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +88 -13
  279. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +87 -12
  280. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +87 -12
  281. nautobot/project-static/docs/user-guide/administration/installation/index.html +87 -12
  282. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +88 -13
  283. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +93 -18
  284. nautobot/project-static/docs/user-guide/administration/installation/services.html +88 -13
  285. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +87 -12
  286. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +87 -12
  287. nautobot/project-static/docs/user-guide/administration/security/index.html +9420 -0
  288. nautobot/project-static/docs/user-guide/administration/security/notices.html +9844 -0
  289. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +87 -12
  290. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +88 -13
  291. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +87 -12
  292. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +87 -12
  293. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +87 -12
  294. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +87 -12
  295. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +87 -12
  296. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +87 -12
  297. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +87 -12
  298. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +98 -20
  299. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +87 -12
  300. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +87 -12
  301. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +87 -12
  302. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +87 -12
  303. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +87 -12
  304. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +87 -12
  305. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +87 -12
  306. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +87 -12
  307. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +87 -12
  308. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +87 -12
  309. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +87 -12
  310. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +87 -12
  311. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +87 -12
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +87 -12
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +87 -12
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +87 -12
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +87 -12
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +87 -12
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +87 -12
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +87 -12
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +87 -12
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +87 -12
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +87 -12
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +87 -12
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +87 -12
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +87 -12
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +87 -12
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +87 -12
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +87 -12
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +87 -12
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +87 -12
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +87 -12
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +87 -12
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +87 -12
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +87 -12
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +87 -12
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +87 -12
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +87 -12
  337. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +87 -12
  338. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +87 -12
  339. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +87 -12
  340. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +87 -12
  341. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +87 -12
  342. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +87 -12
  343. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +87 -12
  344. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +87 -12
  345. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +87 -12
  346. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +87 -12
  347. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +87 -12
  348. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +87 -12
  349. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +87 -12
  350. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +87 -12
  351. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +87 -12
  352. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +87 -12
  353. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +87 -12
  354. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +87 -12
  355. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +87 -12
  356. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +87 -12
  357. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +87 -12
  358. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +87 -12
  359. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +87 -12
  360. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +87 -12
  361. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +87 -12
  362. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +87 -12
  363. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +87 -12
  364. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +87 -12
  365. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +87 -12
  366. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +87 -12
  367. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +87 -12
  368. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +87 -12
  369. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +87 -12
  370. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +87 -12
  371. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +87 -12
  372. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +87 -12
  373. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +87 -12
  374. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +87 -12
  375. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +87 -12
  376. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +87 -12
  377. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +87 -12
  378. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +87 -12
  379. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +87 -12
  380. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +99 -24
  381. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +87 -12
  382. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +87 -12
  383. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +90 -15
  384. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +87 -12
  385. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +87 -12
  386. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +87 -12
  387. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +87 -12
  388. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +87 -12
  389. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +87 -12
  390. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +188 -30
  391. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +87 -12
  392. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +87 -12
  393. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +87 -12
  394. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +87 -12
  395. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +87 -12
  396. nautobot/project-static/docs/user-guide/index.html +87 -12
  397. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +87 -12
  398. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +87 -12
  399. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +87 -12
  400. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +87 -12
  401. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +88 -13
  402. nautobot/project-static/docs/user-guide/platform-functionality/events.html +87 -12
  403. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +87 -12
  404. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +87 -12
  405. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +407 -14
  406. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +90 -15
  407. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +87 -12
  408. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +87 -12
  409. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +87 -12
  410. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +87 -12
  411. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +87 -12
  412. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +87 -12
  413. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +89 -14
  414. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +93 -18
  415. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +87 -12
  416. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +87 -12
  417. nautobot/project-static/docs/user-guide/platform-functionality/note.html +87 -12
  418. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +87 -12
  419. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +87 -12
  420. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +87 -12
  421. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +87 -12
  422. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +87 -12
  423. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +87 -12
  424. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +87 -12
  425. nautobot/project-static/docs/user-guide/platform-functionality/role.html +87 -12
  426. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +87 -12
  427. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +87 -12
  428. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +87 -12
  429. nautobot/project-static/docs/user-guide/platform-functionality/status.html +87 -12
  430. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +87 -12
  431. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +87 -12
  432. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +87 -12
  433. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +87 -12
  434. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +87 -12
  435. nautobot/project-static/js/dropdown.js +28 -0
  436. nautobot/tenancy/forms.py +9 -0
  437. nautobot/tenancy/templates/tenancy/tenant.html +1 -2
  438. nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
  439. nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
  440. nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
  441. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
  442. nautobot/tenancy/tests/test_views.py +5 -1
  443. nautobot/tenancy/urls.py +7 -79
  444. nautobot/tenancy/views.py +51 -80
  445. nautobot/virtualization/templates/virtualization/cluster.html +1 -1
  446. nautobot/virtualization/templates/virtualization/clustergroup.html +1 -1
  447. nautobot/virtualization/templates/virtualization/clustertype.html +1 -1
  448. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  449. nautobot/virtualization/templates/virtualization/vminterface.html +1 -1
  450. nautobot/wireless/api/serializers.py +6 -1
  451. nautobot/wireless/api/views.py +3 -3
  452. nautobot/wireless/tests/test_api.py +5 -0
  453. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/METADATA +12 -12
  454. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/RECORD +459 -443
  455. nautobot/dcim/tests/integration/test_device_bulk_delete.py +0 -189
  456. nautobot/dcim/tests/integration/test_device_bulk_edit.py +0 -181
  457. /nautobot/project-static/docs/assets/stylesheets/{main.6f8fc17f.min.css.map → main.a40c8224.min.css.map} +0 -0
  458. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/LICENSE.txt +0 -0
  459. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/NOTICE +0 -0
  460. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/WHEEL +0 -0
  461. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/entry_points.txt +0 -0
@@ -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
  """
@@ -4,13 +4,34 @@ import uuid
4
4
  from django.core.cache import cache
5
5
 
6
6
  from nautobot.core.testing import TestCase
7
+ from nautobot.dcim.models import Device, LocationType
7
8
  from nautobot.extras.choices import JobQueueTypeChoices
8
9
  from nautobot.extras.models import JobQueue
9
10
  from nautobot.extras.registry import registry
10
- from nautobot.extras.utils import get_celery_queues, get_worker_count, populate_model_features_registry
11
+ from nautobot.extras.utils import (
12
+ get_base_template,
13
+ get_celery_queues,
14
+ get_worker_count,
15
+ populate_model_features_registry,
16
+ )
17
+ from nautobot.users.models import Token
11
18
 
12
19
 
13
20
  class UtilsTestCase(TestCase):
21
+ def test_get_base_template(self):
22
+ with self.subTest("explicitly specified base_template always wins"):
23
+ self.assertEqual(get_base_template("dcim/device/base.html", Device), "dcim/device/base.html")
24
+
25
+ with self.subTest("<model>.html wins over <model>_retrieve.html"):
26
+ # TODO: why do we even have both locationtype.html and locationtype_retrieve.html?
27
+ self.assertEqual(get_base_template(None, LocationType), "dcim/locationtype.html")
28
+
29
+ with self.subTest("<model>_retrieve.html is used if present"):
30
+ self.assertEqual(get_base_template(None, JobQueue), "extras/jobqueue_retrieve.html")
31
+
32
+ with self.subTest("generic/object_retrieve.html is used as a fallback"):
33
+ self.assertEqual(get_base_template(None, Token), "generic/object_retrieve.html")
34
+
14
35
  @mock.patch("celery.app.control.Inspect.active_queues")
15
36
  def test_get_celery_queues(self, mock_active_queues):
16
37
  with self.subTest("No queues"):
@@ -820,7 +820,8 @@ class DynamicGroupTestCase(
820
820
  return super()._get_queryset().filter(group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_FILTER) # TODO
821
821
 
822
822
  def test_get_object_with_permission(self):
823
- instance = self._get_queryset().first()
823
+ location_ct = ContentType.objects.get_for_model(Location)
824
+ instance = self._get_queryset().exclude(content_type=location_ct).first()
824
825
  # Add view permissions for the group's members:
825
826
  self.add_permissions(get_permission_for_model(instance.content_type.model_class(), "view"))
826
827
 
@@ -831,6 +832,18 @@ class DynamicGroupTestCase(
831
832
  for member in instance.members:
832
833
  self.assertIn(str(member.pk), response_body)
833
834
 
835
+ # Test accessing DynamicGroup detail view with a different content type, more specifically, TreeModel
836
+ # https://github.com/nautobot/nautobot/issues/6806
837
+ tree_model_dg = DynamicGroup.objects.create(name="DG 4", content_type=location_ct)
838
+ # Add view permissions for the group's members:
839
+ self.add_permissions(get_permission_for_model(tree_model_dg.content_type.model_class(), "view"))
840
+ response = self.client.get(tree_model_dg.get_absolute_url())
841
+ self.assertHttpStatus(response, 200)
842
+ response_body = extract_page_body(response.content.decode(response.charset))
843
+ # Check that the "members" table in the detail view includes all appropriate member objects
844
+ for member in tree_model_dg.members:
845
+ self.assertIn(str(member.pk), response_body)
846
+
834
847
  def test_get_object_with_constrained_permission(self):
835
848
  instance = self._get_queryset().first()
836
849
  # Add view permission for one of the group's members but not the others:
@@ -1306,19 +1319,18 @@ class SavedViewTest(ModelViewTestCase):
1306
1319
 
1307
1320
  model = SavedView
1308
1321
 
1309
- def get_view_url_for_saved_view(self, saved_view, action="detail"):
1322
+ def get_view_url_for_saved_view(self, saved_view=None, action="detail"):
1310
1323
  """
1311
1324
  Since saved view detail url redirects, we need to manually construct its detail url
1312
1325
  to test the content of its response.
1313
1326
  """
1314
- view = saved_view.view
1315
- pk = saved_view.pk
1327
+ url = ""
1316
1328
 
1317
- if action == "detail":
1318
- url = reverse(view) + f"?saved_view={pk}"
1319
- elif action == "edit":
1329
+ if action == "detail" and saved_view:
1330
+ url = reverse(saved_view.view) + f"?saved_view={saved_view.pk}"
1331
+ elif action == "edit" and saved_view:
1320
1332
  url = saved_view.get_absolute_url() + "update-config/"
1321
- else:
1333
+ elif action == "create":
1322
1334
  url = reverse("extras:savedview_add")
1323
1335
 
1324
1336
  return url
@@ -1411,7 +1423,14 @@ class SavedViewTest(ModelViewTestCase):
1411
1423
 
1412
1424
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1413
1425
  def test_update_saved_view_as_owner(self):
1414
- instance = self._get_queryset().first()
1426
+ view_name = "dcim:location_list"
1427
+ instance = SavedView.objects.create(
1428
+ name="Location Saved View",
1429
+ owner=self.user,
1430
+ view=view_name,
1431
+ is_global_default=True,
1432
+ )
1433
+
1415
1434
  update_query_strings = ["per_page=12", "&status=active", "&name=new_name_filter", "&sort=name"]
1416
1435
  update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1417
1436
  # Try update the saved view with the same user as the owner of the saved view
@@ -1543,6 +1562,62 @@ class SavedViewTest(ModelViewTestCase):
1543
1562
  # Assert that Location List View got redirected to Saved View set as user default
1544
1563
  self.assertBodyContains(response, "<strong>User Location Default View</strong>", html=True)
1545
1564
 
1565
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1566
+ def test_filtered_view_precedes_global_default(self):
1567
+ view_name = "dcim:location_list"
1568
+ # Global saved view that will show Floor type locations only.
1569
+ SavedView.objects.create(
1570
+ name="Global Location Default View",
1571
+ owner=self.user,
1572
+ view=view_name,
1573
+ is_global_default=True,
1574
+ config={
1575
+ "filter_params": {
1576
+ "location_type": ["Floor"],
1577
+ }
1578
+ },
1579
+ )
1580
+ response = self.client.get(reverse(view_name) + "?location_type=Campus", follow=True)
1581
+ # Assert that the user is not redirected to the global default view
1582
+ # But instead redirected to the filtered view
1583
+ self.assertNotIn(
1584
+ "<strong>Global Location Default View</strong>",
1585
+ extract_page_body(response.content.decode(response.charset)),
1586
+ )
1587
+
1588
+ # Floor type locations (Floor-<number>) should not be visible in the response
1589
+ self.assertNotIn(
1590
+ "Floor-",
1591
+ extract_page_body(response.content.decode(response.charset)),
1592
+ )
1593
+
1594
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1595
+ def test_filtered_view_precedes_user_default(self):
1596
+ view_name = "dcim:location_list"
1597
+ # User saved view that will show Floor type locations only.
1598
+ sv = SavedView.objects.create(
1599
+ name="User Location Default View",
1600
+ owner=self.user,
1601
+ view=view_name,
1602
+ config={
1603
+ "filter_params": {
1604
+ "location_type": ["Floor"],
1605
+ }
1606
+ },
1607
+ )
1608
+ UserSavedViewAssociation.objects.create(user=self.user, saved_view=sv, view_name=sv.view)
1609
+ response = self.client.get(reverse(view_name) + "?location_type=Campus", follow=True)
1610
+ # Assert that the user is not redirected to the user default view
1611
+ # But instead redirected to the filtered view
1612
+ self.assertNotIn(
1613
+ "<strong>User Location Default View</strong>", extract_page_body(response.content.decode(response.charset))
1614
+ )
1615
+ # Floor type locations (Floor-<number>) should not be visible in the response
1616
+ self.assertNotIn(
1617
+ "Floor-",
1618
+ extract_page_body(response.content.decode(response.charset)),
1619
+ )
1620
+
1546
1621
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1547
1622
  def test_is_shared(self):
1548
1623
  view_name = "dcim:location_list"
@@ -1568,6 +1643,119 @@ class SavedViewTest(ModelViewTestCase):
1568
1643
  self.assertIn(str(sv_shared.pk), response_body, msg=response_body)
1569
1644
  self.assertNotIn(str(sv_not_shared.pk), response_body, msg=response_body)
1570
1645
 
1646
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1647
+ def test_create_saved_views_contain_boolean_filter_params(self):
1648
+ """
1649
+ Test the entire Save View workflow from creating a Saved View to rendering the View with boolean filter parameters.
1650
+ """
1651
+ with self.subTest("Create job Saved View with boolean filter parameters"):
1652
+ view_name = "extras:job_list"
1653
+ app_label = view_name.split(":")[0]
1654
+ model_name = view_name.split(":")[1].split("_")[0]
1655
+ self.add_permissions(f"{app_label}.view_{model_name}")
1656
+ create_query_strings = [
1657
+ "&hidden=True",
1658
+ ]
1659
+ create_url = self.get_view_url_for_saved_view(action="create")
1660
+ sv_name = "Hidden Jobs"
1661
+ request = {
1662
+ "path": create_url,
1663
+ "data": post_data({"name": sv_name, "view": f"{view_name}", "params": "".join(create_query_strings)}),
1664
+ }
1665
+ self.assertHttpStatus(self.client.post(**request), 302)
1666
+ instance = SavedView.objects.get(name=sv_name)
1667
+ hidden_job = Job.objects.get(name="Example hidden job")
1668
+ hidden_job.description = "I should not show in the UI!"
1669
+ hidden_job.save()
1670
+ self.assertEqual(instance.config["filter_params"]["hidden"], "True")
1671
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1672
+ # Assert that Job List View rendered with the boolean filter parameter without error
1673
+ self.assertHttpStatus(response, 200)
1674
+ response_body = extract_page_body(response.content.decode(response.charset))
1675
+ self.assertIn(str(instance.pk), response_body, msg=response_body)
1676
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1677
+ # This is the description
1678
+ self.assertBodyContains(response, "I should not show in the UI!", html=True)
1679
+
1680
+ with self.subTest("Create device Saved View with boolean filter parameters"):
1681
+ view_name = "dcim:device_list"
1682
+ app_label = view_name.split(":")[0]
1683
+ model_name = view_name.split(":")[1].split("_")[0]
1684
+ self.add_permissions(f"{app_label}.view_{model_name}")
1685
+ create_query_strings = [
1686
+ "&per_page=12",
1687
+ "&has_primary_ip=True",
1688
+ "&sort=name",
1689
+ ]
1690
+ create_url = self.get_view_url_for_saved_view(action="create")
1691
+ sv_name = "Devices with primary ips"
1692
+ request = {
1693
+ "path": create_url,
1694
+ "data": post_data({"name": sv_name, "view": f"{view_name}", "params": "".join(create_query_strings)}),
1695
+ }
1696
+ self.assertHttpStatus(self.client.post(**request), 302)
1697
+ instance = SavedView.objects.get(name=sv_name)
1698
+ self.assertEqual(instance.config["pagination_count"], 12)
1699
+ self.assertEqual(instance.config["filter_params"]["has_primary_ip"], "True")
1700
+ self.assertEqual(instance.config["sort_order"], ["name"])
1701
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1702
+ # Assert that Job List View rendered with the boolean filter parameter without error
1703
+ self.assertHttpStatus(response, 200)
1704
+ response_body = extract_page_body(response.content.decode(response.charset))
1705
+ self.assertIn(str(instance.pk), response_body, msg=response_body)
1706
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1707
+
1708
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1709
+ def test_update_saved_view_contain_boolean_filter_params(self):
1710
+ with self.subTest("Update job Saved View with boolean filter parameters"):
1711
+ view_name = "extras:job_list"
1712
+ sv_name = "Non-hidden jobs"
1713
+ instance = SavedView.objects.create(
1714
+ name=sv_name,
1715
+ owner=self.user,
1716
+ view=view_name,
1717
+ )
1718
+ update_query_strings = ["hidden=False"]
1719
+ update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1720
+ # Try update the saved view with the same user as the owner of the saved view
1721
+ instance.owner.is_active = True
1722
+ instance.owner.save()
1723
+ self.client.force_login(instance.owner)
1724
+ response = self.client.get(update_url)
1725
+ self.assertHttpStatus(response, 302)
1726
+ instance.refresh_from_db()
1727
+ self.assertEqual(instance.config["filter_params"]["hidden"], "False")
1728
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1729
+ # Assert that Job List View rendered with the boolean filter parameter without error
1730
+ self.assertHttpStatus(response, 200)
1731
+ response_body = extract_page_body(response.content.decode(response.charset))
1732
+ self.assertNotIn("Example hidden job", response_body, msg=response_body)
1733
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1734
+
1735
+ with self.subTest("Update device Saved View with boolean filter parameters"):
1736
+ view_name = "dcim:device_list"
1737
+ sv_name = "Devices with no primary ips"
1738
+ instance = SavedView.objects.create(
1739
+ name=sv_name,
1740
+ owner=self.user,
1741
+ view=view_name,
1742
+ )
1743
+ update_query_strings = ["has_primary_ip=False"]
1744
+ update_url = self.get_view_url_for_saved_view(instance, "edit") + "?" + "".join(update_query_strings)
1745
+ # Try update the saved view with the same user as the owner of the saved view
1746
+ instance.owner.is_active = True
1747
+ instance.owner.save()
1748
+ self.client.force_login(instance.owner)
1749
+ response = self.client.get(update_url)
1750
+ self.assertHttpStatus(response, 302)
1751
+ instance.refresh_from_db()
1752
+ self.assertEqual(instance.config["filter_params"]["has_primary_ip"], "False")
1753
+ response = self.client.get(reverse(view_name) + "?saved_view=" + str(instance.pk), follow=True)
1754
+ # Assert that Job List View rendered with the boolean filter parameter without error
1755
+ self.assertHttpStatus(response, 200)
1756
+ response_body = extract_page_body(response.content.decode(response.charset))
1757
+ self.assertBodyContains(response, f"<strong>{sv_name}</strong>", html=True)
1758
+
1571
1759
 
1572
1760
  # Not a full-fledged PrimaryObjectViewTestCase as there's no BulkEditView for Secrets
1573
1761
  class SecretTestCase(