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
@@ -0,0 +1,43 @@
1
+ import uuid
2
+
3
+ from nautobot.circuits.models import Circuit, CircuitType, Provider
4
+ from nautobot.core.testing.integration import (
5
+ BulkOperationsTestCases,
6
+ )
7
+ from nautobot.extras.models import Status
8
+
9
+
10
+ class CircuitBulkOperationsTestCase(BulkOperationsTestCases.BulkOperationsTestCase):
11
+ """
12
+ Test circuits bulk edit / delete operations.
13
+ """
14
+
15
+ model_menu_path = ("Circuits", "Circuits")
16
+ model_base_viewname = "circuits:circuit"
17
+ model_edit_data = {"commit_rate": "12345"}
18
+ model_filter_by = {"circuit_type": "Copper"}
19
+ model_class = Circuit
20
+
21
+ def setup_items(self):
22
+ Circuit.objects.all().delete()
23
+
24
+ # Create locations for test
25
+ self.create_circuit()
26
+ self.create_circuit()
27
+ self.create_circuit()
28
+ self.create_circuit("Copper")
29
+ self.create_circuit("Copper")
30
+
31
+ @staticmethod
32
+ def create_circuit(circuit_type="Fiber"):
33
+ circuit_id = f"TestCircuit-{str(uuid.uuid4())[:6]}"
34
+ provider, _ = Provider.objects.get_or_create(name="A Test Provider")
35
+ circuit_type, _ = CircuitType.objects.get_or_create(name=circuit_type)
36
+
37
+ circuit_status = Status.objects.get_for_model(Circuit).first()
38
+ Circuit.objects.get_or_create(
39
+ cid=circuit_id,
40
+ provider=provider,
41
+ status=circuit_status,
42
+ circuit_type=circuit_type,
43
+ )
@@ -19,7 +19,7 @@ class CircuitRelationshipsTestCase(SeleniumTestCase):
19
19
  self.user.is_superuser = True
20
20
  self.user.save()
21
21
  self.login(self.user.username, self.password)
22
- location_type = LocationType.objects.get(name="Campus")
22
+ location_type, _ = LocationType.objects.get_or_create(name="Campus")
23
23
  location_ct = ContentType.objects.get_for_model(Location)
24
24
  circuit_termination_ct = ContentType.objects.get_for_model(CircuitTermination)
25
25
  provider_ct = ContentType.objects.get_for_model(Provider)
@@ -367,11 +367,6 @@ class CoreConfig(NautobotConfig):
367
367
 
368
368
  super().ready()
369
369
 
370
- # Register jobs last after everything else has been done.
371
- from nautobot.core.celery import import_jobs
372
-
373
- import_jobs()
374
-
375
370
 
376
371
  class NautobotConstanceConfig(ConstanceConfig):
377
372
  """Override "Constance" app name to "Configuration"."""
@@ -7,7 +7,7 @@
7
7
  {% block content %}
8
8
  <div class="row">
9
9
  <div class="col-md-8 col-md-offset-2">
10
- <div class="panel panel-danger">
10
+ <div class="panel panel-danger" id="confirm-bulk-deletion">
11
11
  <div class="panel-heading"><strong>Confirm Bulk Deletion</strong></div>
12
12
  <div class="panel-body">
13
13
  <p><strong>Warning:</strong> The following operation will delete {{ total_objs_to_delete }} {{ obj_type_plural }}. {% if not delete_all %}Please carefully review the {{ obj_type_plural }} to be deleted and confirm below.{% endif %}</p>
@@ -1,7 +1,9 @@
1
1
  import os
2
+ from typing import Any, Optional
2
3
 
3
4
  from django.conf import settings
4
5
  from django.contrib.staticfiles.testing import StaticLiveServerTestCase
6
+ from django.db.models import Model
5
7
  from django.test import override_settings, tag
6
8
  from django.urls import reverse
7
9
  from django.utils.functional import classproperty
@@ -16,7 +18,7 @@ from nautobot.core import testing
16
18
  SELENIUM_URL = os.getenv("NAUTOBOT_SELENIUM_URL", "http://localhost:4444/wd/hub")
17
19
 
18
20
  # Hostname used by Selenium client to talk to Nautobot
19
- SELENIUM_HOST = os.getenv("NAUTOBOT_SELENIUM_HOST", "host.docker.internal")
21
+ SELENIUM_HOST = os.getenv("NAUTOBOT_SELENIUM_HOST", "nautobot")
20
22
 
21
23
  # Default login URL
22
24
  LOGIN_URL = reverse(settings.LOGIN_URL)
@@ -28,23 +30,68 @@ class ObjectsListMixin:
28
30
  """
29
31
 
30
32
  def select_all_items(self):
31
- self.browser.find_by_xpath('//*[@id="object_list_form"]//input[@class="toggle"]').click()
33
+ """
34
+ Click "toggle all" on top of the items table list to select all rows.
35
+ """
36
+ self.browser.find_by_css("#object_list_form input.toggle").click()
32
37
 
33
38
  def select_one_item(self):
34
- self.browser.find_by_xpath('//*[@id="object_list_form"]//input[@name="pk"]').click()
39
+ """
40
+ Click first row checkbox on items table list to select one row.
41
+ """
42
+ self.browser.find_by_css('#object_list_form input[name="pk"]').click()
43
+
44
+ def set_per_page(self, per_page=1):
45
+ """
46
+ Explicitly set the `per_page` parameter by navigating to the "current" page but with query param.
47
+ TODO: check if there are other query params and merge them
48
+ """
49
+ self.browser.visit(f"{self.browser.url}?per_page={per_page}")
50
+
51
+ def select_all_items_from_all_pages(self):
52
+ """
53
+ Selecting all the items from all pages by clicking "select all" on top of the items table list and then
54
+ select all on prompt that will show up.
55
+ """
56
+ self.select_all_items()
57
+ self.browser.find_by_css("#select_all").click()
35
58
 
36
59
  def click_bulk_delete(self):
60
+ """
61
+ Click bulk delete from dropdown menu on bottom of the items table list.
62
+ """
63
+ self.browser.execute_script(
64
+ "document.querySelector('#object_list_form button[type=\"submit\"]').scrollIntoView()"
65
+ )
37
66
  self.browser.find_by_xpath(
38
67
  '//*[@id="object_list_form"]//button[@type="submit"]/following-sibling::button[1]'
39
68
  ).click()
40
- self.browser.find_by_xpath('//*[@id="object_list_form"]//button[@name="_delete"]').click()
69
+ self.browser.find_by_css('#object_list_form button[name="_delete"]').click()
70
+
71
+ def click_bulk_delete_all(self):
72
+ """
73
+ Click bulk delete all on prompt when selecting all items from all pages.
74
+ """
75
+ self.click_button('#select_all_box button[name="_delete"]')
41
76
 
42
77
  def click_bulk_edit(self):
43
- self.browser.find_by_xpath('//*[@id="object_list_form"]//button[@type="submit"]').click()
78
+ """
79
+ Click bulk edit button on bottom of the items table list.
80
+ """
81
+ self.click_button('#object_list_form button[type="submit"]')
82
+
83
+ def click_bulk_edit_all(self):
84
+ """
85
+ Click bulk edit all on prompt when selecting all items from all pages.
86
+ """
87
+ self.click_button('#select_all_box button[name="_edit"]')
44
88
 
45
89
  @property
46
90
  def objects_list_visible_items(self):
47
- objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]/div[1]/div')
91
+ """
92
+ Calculating the visible items. Return 0 if there is no visible items.
93
+ """
94
+ objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]')
48
95
  try:
49
96
  objects_table = objects_table_container.find_by_tag("tbody")
50
97
  return len(objects_table.find_by_tag("tr"))
@@ -52,19 +99,32 @@ class ObjectsListMixin:
52
99
  return 0
53
100
 
54
101
  def apply_filter(self, field, value):
102
+ """
103
+ Open filter dialog and apply select2 filters.
104
+ You can apply more values to the same filter, by calling this function with same name but different value.
105
+ """
55
106
  self.browser.find_by_xpath('//*[@id="id__filterbtn"]').click()
56
107
  self.fill_filters_select2_field(field, value)
57
- self.browser.find_by_xpath('//*[@id="default-filter"]//button[@type="submit"]').click()
108
+ self.click_button('#default-filter button[type="submit"]')
58
109
 
59
110
 
60
111
  class BulkOperationsMixin:
61
112
  def confirm_bulk_delete_operation(self):
62
- self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').click()
113
+ """
114
+ Confirms bulk delete operation on the "warning" page after clicking bulk delete buttons.
115
+ """
116
+ self.click_button('button[name="_confirm"][type="submit"]')
63
117
 
64
118
  def submit_bulk_edit_operation(self):
65
- self.browser.find_by_xpath("//button[@name='_apply']", wait_time=5).click()
119
+ """
120
+ Submits the bulk edit form.
121
+ """
122
+ self.click_button('button[name="_apply"]')
66
123
 
67
124
  def wait_for_job_result(self):
125
+ """
126
+ Waits 30s for job to be finished.
127
+ """
68
128
  end_statuses = ["Completed", "Failed"]
69
129
  WebDriverWait(self.browser, 30).until(
70
130
  lambda driver: driver.find_by_id("pending-result-label").text in end_statuses
@@ -73,16 +133,52 @@ class BulkOperationsMixin:
73
133
  return self.browser.find_by_id("pending-result-label").text
74
134
 
75
135
  def verify_job_description(self, expected_job_description):
136
+ """
137
+ Verifies if the job description is correct.
138
+ Waits 30s on page load in case of large payload being sent from bulk edit form.
139
+ """
140
+ WebDriverWait(self.browser, 30).until(lambda driver: driver.is_text_present("Job Description"))
141
+
76
142
  job_description = self.browser.find_by_xpath('//td[text()="Job Description"]/following-sibling::td[1]').text
77
143
  self.assertEqual(job_description, expected_job_description)
78
144
 
145
+ def update_edit_form_value(self, field_name, value, is_select=False):
146
+ """
147
+ Updates bulk edit form value.
148
+ """
149
+ if is_select:
150
+ self.fill_select2_field(field_name, value)
151
+ else:
152
+ self.browser.fill(field_name, value)
153
+
154
+ def assertBulkDeleteConfirmMessageIsValid(self, expected_count):
155
+ """
156
+ Asserts that bulk delete confirmation message is valid and if we're deleting proper number of items.
157
+ """
158
+ self.browser.is_element_present_by_tag("body", wait_time=30)
159
+
160
+ button_text = self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').text
161
+ self.assertIn(f"Delete these {expected_count}", button_text)
162
+
163
+ message_text = self.browser.find_by_id("confirm-bulk-deletion").find_by_xpath('//div[@class="panel-body"]').text
164
+ self.assertIn(f"The following operation will delete {expected_count}", message_text)
165
+
79
166
  def assertIsBulkDeleteJob(self):
167
+ """
168
+ Asserts if currently visible job is bulk delete job.
169
+ """
80
170
  self.verify_job_description("Bulk delete objects.")
81
171
 
82
172
  def assertIsBulkEditJob(self):
173
+ """
174
+ Asserts if currently visible job is bulk edit job.
175
+ """
83
176
  self.verify_job_description("Bulk edit objects.")
84
177
 
85
178
  def assertJobStatusIsCompleted(self):
179
+ """
180
+ Asserts that job was successfully completed.
181
+ """
86
182
  job_status = self.wait_for_job_result()
87
183
  self.assertEqual(job_status, "Completed")
88
184
 
@@ -207,7 +303,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
207
303
  Helper function to fill a Select2 single selection field on filters modals.
208
304
  """
209
305
  self._fill_select2_field(field_name, value, search_box_class="select2-search select2-search--inline")
210
- self.browser.find_by_xpath(f"//li[@class='select2-results__option' and text()='{value}']").click()
306
+ self.browser.find_by_xpath(f"//li[contains(@class, 'select2-results__option') and text()='{value}']").click()
211
307
 
212
308
  def fill_select2_multiselect_field(self, field_name, value):
213
309
  """
@@ -220,3 +316,334 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
220
316
  # wait for "searching" to disappear
221
317
  self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
222
318
  search_box.first.type(Keys.ENTER)
319
+
320
+ def click_button(self, query_selector):
321
+ btn = self.browser.find_by_css(query_selector, wait_time=5)
322
+ # Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
323
+ self.browser.execute_script(f"document.querySelector('{query_selector}').scrollIntoView()")
324
+ btn.click()
325
+
326
+ def login_as_superuser(self):
327
+ self.user.is_superuser = True
328
+ self.user.save()
329
+ self.login(self.user.username, self.password)
330
+
331
+
332
+ class BulkOperationsTestCases:
333
+ """
334
+ Helper classes that runs all the basic bulk-operations test cases like edit/delete with
335
+ filtered / not filtered items along with select all option.
336
+
337
+ To use this class create required items in setUp method:
338
+ - at least four entities in two different groups,
339
+ - provide field for filtering to distinguish between above two groups
340
+ - provide expected counts (if different from default)
341
+ - set edit field and value
342
+ """
343
+
344
+ class BaseTestCase(SeleniumTestCase):
345
+ model_menu_path: tuple[str, str]
346
+ model_base_viewname: str
347
+ model_edit_data: dict[str, Any]
348
+ model_filter_by: dict[str, Any]
349
+ model_class: type[Model]
350
+ override_model_plural: Optional[str] = None
351
+ model_expected_counts: dict[str, int] = {
352
+ "all": 5,
353
+ "filtered": 2,
354
+ }
355
+
356
+ @property
357
+ def model_plural(self) -> str:
358
+ if self.override_model_plural is None:
359
+ return self.model_class._meta.verbose_name_plural
360
+
361
+ return self.override_model_plural
362
+
363
+ def setUp(self):
364
+ super().setUp()
365
+
366
+ self.setup_items()
367
+ self.login_as_superuser()
368
+ self.go_to_model_list_page()
369
+
370
+ def tearDown(self):
371
+ self.logout()
372
+ super().tearDown()
373
+
374
+ def go_to_model_list_page(self):
375
+ self.click_navbar_entry(*self.model_menu_path)
376
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
377
+
378
+ def setup_items(self):
379
+ raise NotImplementedError
380
+
381
+ class BulkEditTestCase(BaseTestCase, ObjectsListMixin, BulkOperationsMixin):
382
+ def test_bulk_edit_require_selection(self):
383
+ # Click "edit selected" without selecting anything
384
+ self.click_bulk_edit()
385
+
386
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
387
+ self.assertTrue(self.browser.is_text_present(f"No {self.model_plural} were selected", wait_time=5))
388
+
389
+ def test_bulk_edit_all_items(self):
390
+ # Select all items and edit them
391
+ self.select_all_items()
392
+ self.click_bulk_edit()
393
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"))
394
+
395
+ # Edit some data and submit the form
396
+ for field_name, field_value in self.model_edit_data.items():
397
+ self.update_edit_form_value(field_name, field_value)
398
+ self.submit_bulk_edit_operation()
399
+
400
+ # Verify job output
401
+ self.assertIsBulkEditJob()
402
+ self.assertJobStatusIsCompleted()
403
+
404
+ # Assert that data was changed
405
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
406
+ self.assertEqual(found_items, self.model_expected_counts["all"])
407
+
408
+ def test_bulk_edit_one_item(self):
409
+ # Select one filtered item
410
+ self.select_one_item()
411
+ self.click_bulk_edit()
412
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"))
413
+
414
+ # Edit some data and submit the form
415
+ for field_name, field_value in self.model_edit_data.items():
416
+ self.update_edit_form_value(field_name, field_value)
417
+ self.submit_bulk_edit_operation()
418
+
419
+ # Verify job output
420
+ self.assertIsBulkEditJob()
421
+ self.assertJobStatusIsCompleted()
422
+
423
+ # Assert that data was changed
424
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
425
+ self.assertEqual(found_items, 1)
426
+
427
+ def test_bulk_edit_all_items_from_all_pages(self):
428
+ # Select all from all pages
429
+ self.set_per_page()
430
+ self.select_all_items_from_all_pages()
431
+ self.click_bulk_edit_all()
432
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"), self.browser.url)
433
+
434
+ # Edit some data and submit the form
435
+ for field_name, field_value in self.model_edit_data.items():
436
+ self.update_edit_form_value(field_name, field_value)
437
+ self.submit_bulk_edit_operation()
438
+
439
+ # Verify job output
440
+ self.assertIsBulkEditJob()
441
+ self.assertJobStatusIsCompleted()
442
+
443
+ # Assert that data was changed
444
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
445
+ self.assertEqual(found_items, self.model_expected_counts["all"])
446
+
447
+ def test_bulk_edit_all_filtered_items(self):
448
+ # Filter items
449
+ for field, value in self.model_filter_by.items():
450
+ self.apply_filter(field, value)
451
+
452
+ # Select all filtered items
453
+ self.select_all_items()
454
+ self.click_bulk_edit()
455
+ self.assertIn(
456
+ self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"),
457
+ self.browser.url,
458
+ )
459
+
460
+ # Edit some data and submit the form
461
+ for field_name, field_value in self.model_edit_data.items():
462
+ self.update_edit_form_value(field_name, field_value)
463
+ self.submit_bulk_edit_operation()
464
+
465
+ # Verify job output
466
+ self.assertIsBulkEditJob()
467
+ self.assertJobStatusIsCompleted()
468
+
469
+ # Assert that data was changed
470
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
471
+ self.assertEqual(found_items, self.model_expected_counts["filtered"])
472
+
473
+ def test_bulk_edit_one_filtered_item(self):
474
+ # Filter items
475
+ for field, value in self.model_filter_by.items():
476
+ self.apply_filter(field, value)
477
+
478
+ # Select one item and edit it
479
+ self.select_one_item()
480
+ self.click_bulk_edit()
481
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"), self.browser.url)
482
+
483
+ # Edit some data and submit the form
484
+ for field_name, field_value in self.model_edit_data.items():
485
+ self.update_edit_form_value(field_name, field_value)
486
+ self.submit_bulk_edit_operation()
487
+
488
+ # Verify job output
489
+ self.assertIsBulkEditJob()
490
+ self.assertJobStatusIsCompleted()
491
+
492
+ # Assert that data was changed
493
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
494
+ self.assertEqual(found_items, 1)
495
+
496
+ def test_bulk_edit_all_filtered_items_from_all_pages(self):
497
+ # Filter items
498
+ self.set_per_page()
499
+ for field, value in self.model_filter_by.items():
500
+ self.apply_filter(field, value)
501
+
502
+ # Select all items and delete them
503
+ self.select_all_items_from_all_pages()
504
+ self.click_bulk_edit_all()
505
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"), self.browser.url)
506
+
507
+ # Edit some data and submit the form
508
+ for field_name, field_value in self.model_edit_data.items():
509
+ self.update_edit_form_value(field_name, field_value)
510
+ self.submit_bulk_edit_operation()
511
+
512
+ # Verify job output
513
+ self.assertIsBulkEditJob()
514
+ self.assertJobStatusIsCompleted()
515
+
516
+ # Assert that data was changed
517
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
518
+ self.assertEqual(found_items, self.model_expected_counts["filtered"])
519
+
520
+ class BulkDeleteTestCase(BaseTestCase, ObjectsListMixin, BulkOperationsMixin):
521
+ def test_bulk_delete_require_selection(self):
522
+ # Click "delete selected" without selecting anything
523
+ self.click_bulk_delete()
524
+
525
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
526
+ self.assertTrue(
527
+ self.browser.is_text_present(f"No {self.model_plural} were selected for deletion.", wait_time=5)
528
+ )
529
+
530
+ def test_bulk_delete_all_items(self):
531
+ # Select all items and delete them
532
+ self.select_all_items()
533
+ self.click_bulk_delete()
534
+
535
+ self.assertEqual(
536
+ self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete")
537
+ )
538
+ self.assertBulkDeleteConfirmMessageIsValid(self.model_expected_counts["all"])
539
+ self.confirm_bulk_delete_operation()
540
+
541
+ # Verify job output
542
+ self.assertIsBulkDeleteJob()
543
+ self.assertJobStatusIsCompleted()
544
+
545
+ self.go_to_model_list_page()
546
+ self.assertEqual(self.objects_list_visible_items, 0)
547
+
548
+ def test_bulk_delete_one_item(self):
549
+ # Select one item and delete it
550
+ self.select_one_item()
551
+ self.click_bulk_delete()
552
+
553
+ self.assertEqual(
554
+ self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete")
555
+ )
556
+ self.assertBulkDeleteConfirmMessageIsValid(1)
557
+ self.confirm_bulk_delete_operation()
558
+
559
+ # Verify job output
560
+ self.assertIsBulkDeleteJob()
561
+ self.assertJobStatusIsCompleted()
562
+
563
+ self.go_to_model_list_page()
564
+ self.assertEqual(self.objects_list_visible_items, self.model_expected_counts["all"] - 1)
565
+
566
+ def test_bulk_delete_all_items_from_all_pages(self):
567
+ # Select all from all pages
568
+ self.set_per_page()
569
+ self.select_all_items_from_all_pages()
570
+ self.click_bulk_delete_all()
571
+
572
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
573
+ self.assertBulkDeleteConfirmMessageIsValid(self.model_expected_counts["all"])
574
+ self.confirm_bulk_delete_operation()
575
+
576
+ # Verify job output
577
+ self.assertIsBulkDeleteJob()
578
+ self.assertJobStatusIsCompleted()
579
+
580
+ self.go_to_model_list_page()
581
+ self.assertEqual(self.objects_list_visible_items, 0)
582
+
583
+ def test_bulk_delete_all_filtered_items(self):
584
+ # Filter items
585
+ for field, value in self.model_filter_by.items():
586
+ self.apply_filter(field, value)
587
+
588
+ # Select all items and delete them
589
+ self.select_all_items()
590
+ self.click_bulk_delete()
591
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
592
+ self.confirm_bulk_delete_operation()
593
+
594
+ # Verify job output
595
+ self.assertIsBulkDeleteJob()
596
+ self.assertJobStatusIsCompleted()
597
+
598
+ self.go_to_model_list_page()
599
+ rest_items_count = self.model_expected_counts["all"] - self.model_expected_counts["filtered"]
600
+ self.assertEqual(self.objects_list_visible_items, rest_items_count)
601
+
602
+ def test_bulk_delete_one_filtered_items(self):
603
+ # Filter items
604
+ for field, value in self.model_filter_by.items():
605
+ self.apply_filter(field, value)
606
+
607
+ # Select one item and delete it
608
+ self.select_one_item()
609
+ self.click_bulk_delete()
610
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
611
+ self.confirm_bulk_delete_operation()
612
+
613
+ # Verify job output
614
+ self.assertIsBulkDeleteJob()
615
+ self.assertJobStatusIsCompleted()
616
+
617
+ self.go_to_model_list_page()
618
+ self.assertEqual(self.objects_list_visible_items, self.model_expected_counts["all"] - 1)
619
+
620
+ def test_bulk_delete_all_filtered_items_from_all_pages(self):
621
+ # Filter items
622
+ self.set_per_page()
623
+ for field, value in self.model_filter_by.items():
624
+ self.apply_filter(field, value)
625
+
626
+ # Select all items and delete them
627
+ self.select_all_items_from_all_pages()
628
+ self.click_bulk_delete_all()
629
+
630
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
631
+ self.assertBulkDeleteConfirmMessageIsValid(self.model_expected_counts["filtered"])
632
+ self.confirm_bulk_delete_operation()
633
+
634
+ # Verify job output
635
+ self.assertIsBulkDeleteJob()
636
+ self.assertJobStatusIsCompleted()
637
+
638
+ self.go_to_model_list_page()
639
+ self.set_per_page(50) # Set page size back to default
640
+ rest_items_count = self.model_expected_counts["all"] - self.model_expected_counts["filtered"]
641
+ self.assertEqual(self.objects_list_visible_items, rest_items_count)
642
+
643
+ # Filter again and assert that all items were deleted
644
+ for field, value in self.model_filter_by.items():
645
+ self.apply_filter(field, value)
646
+ self.assertEqual(self.objects_list_visible_items, 0)
647
+
648
+ class BulkOperationsTestCase(BulkEditTestCase, BulkDeleteTestCase):
649
+ pass
@@ -750,16 +750,48 @@ class BulkEditTestCase(TransactionTestCase):
750
750
  description="Example description for bulk edit",
751
751
  )
752
752
 
753
- # Assert Namespaces withing pk_list updated tags
753
+ # Assert Namespaces within pk_list get updated tags
754
754
  for namespace in namespaces[:3]:
755
755
  self.assertTrue(namespace.tags.filter(pk__in=[tag.pk for tag in self.tags[:3]]).exists())
756
756
  self.assertFalse(namespace.tags.filter(pk__in=[tag.pk for tag in self.tags[3:]]).exists())
757
757
 
758
- # Assert Namespaces not withing pk_list tags did not get updated
758
+ # Assert Namespaces not within pk_list did not get updated tags
759
759
  for namespace in namespaces[3:]:
760
760
  self.assertFalse(namespace.tags.filter(pk__in=[tag.pk for tag in self.tags[:3]]).exists())
761
761
  self.assertTrue(namespace.tags.filter(pk__in=[tag.pk for tag in self.tags[3:]]).exists())
762
762
 
763
+ job_result = create_job_result_and_run_job(
764
+ "nautobot.core.jobs.bulk_actions",
765
+ "BulkEditObjects",
766
+ content_type=self.namespace_ct.id,
767
+ edit_all=False,
768
+ filter_query_params={},
769
+ form_data={
770
+ "pk": pk_list,
771
+ "description": "Example description for bulk edit",
772
+ "add_tags": [str(self.tags[0].id)],
773
+ "remove_tags": [str(self.tags[-1].id)],
774
+ },
775
+ username=self.user.username,
776
+ )
777
+
778
+ self._common_no_error_test_assertion(
779
+ Namespace,
780
+ job_result,
781
+ 3,
782
+ description="Example description for bulk edit",
783
+ )
784
+
785
+ # Assert Namespaces within pk_list get updated tag
786
+ for namespace in namespaces[:3]:
787
+ self.assertTrue(namespace.tags.filter(pk__in=[self.tags[0].pk]).exists())
788
+ self.assertFalse(namespace.tags.filter(pk__in=[self.tags[-1].pk]).exists())
789
+
790
+ # Assert Namespaces not within pk_list did not get updated tag
791
+ for namespace in namespaces[3:]:
792
+ self.assertFalse(namespace.tags.filter(pk__in=[self.tags[0].pk]).exists())
793
+ self.assertTrue(namespace.tags.filter(pk__in=[self.tags[-1].pk]).exists())
794
+
763
795
  def test_bulk_edit_objects_filter_all(self):
764
796
  """
765
797
  Bulk edit all of the filtered Status instances.
@@ -57,7 +57,7 @@ class BranchDoesNotExist(Exception):
57
57
 
58
58
 
59
59
  class GitRepo:
60
- def __init__(self, path, url, clone_initially=True):
60
+ def __init__(self, path, url, clone_initially=True, branch=None, depth=0):
61
61
  """
62
62
  Ensure that we have a clone of the given remote Git repository URL at the given local directory path.
63
63
 
@@ -65,6 +65,8 @@ class GitRepo:
65
65
  path (str): path to git repo
66
66
  url (str): git repo url
67
67
  clone_initially (bool): True if the repo needs to be cloned
68
+ branch (str): branch to checkout
69
+ depth (int): depth of the clone
68
70
  """
69
71
  self.url = url
70
72
  self.sanitized_url = sanitize(url)
@@ -73,7 +75,10 @@ class GitRepo:
73
75
  elif clone_initially:
74
76
  # Don't log `url` as it may include authentication details.
75
77
  logger.debug("Cloning git repository to %s...", path)
76
- self.repo = Repo.clone_from(url, to_path=path, env=GIT_ENVIRONMENT)
78
+ if not depth:
79
+ self.repo = Repo.clone_from(url, to_path=path, env=GIT_ENVIRONMENT, branch=branch)
80
+ else:
81
+ self.repo = Repo.clone_from(url, to_path=path, env=GIT_ENVIRONMENT, branch=branch, depth=depth)
77
82
  else:
78
83
  self.repo = Repo.init(path)
79
84
  self.repo.create_remote("origin", url=url)