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
@@ -24,6 +24,8 @@
24
24
  onerror="window.location='{% url 'media_failure' %}?filename=js/theme.js'"></script>
25
25
  <script src="{% versioned_static 'js/table_sorting_indicator.js' %}"
26
26
  onerror="window.location='{% url 'media_failure' %}?filename=js/table_sorting_indicator.js'"></script>
27
+ <script src="{% versioned_static 'js/dropdown.js' %}"
28
+ onerror="window.location='{% url 'media_failure' %}?filename=js/dropdown.js'"></script>
27
29
  <script type="text/javascript">
28
30
  var nautobot_api_path = "{% url 'api-root' %}";
29
31
  var nautobot_csrf_token = "{{ csrf_token }}";
@@ -0,0 +1,5 @@
1
+ {% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
2
+ <input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}{% if widget.attrs.checked %} checked{% endif %}>
3
+ <label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br>
4
+ {{ widget.input_text }}:{% endif %}
5
+ <input type="{{ widget.type }}" name="{{ widget.name }}" class="filestyle" data-placeholder="No file selected" {% include "django/forms/widgets/attrs.html" %}>
@@ -870,9 +870,7 @@ def saved_view_modal(
870
870
  ):
871
871
  from nautobot.extras.forms import SavedViewModalForm
872
872
  from nautobot.extras.models import SavedView
873
-
874
- param_dict = {}
875
- filters_applied = parse_qs(params)
873
+ from nautobot.extras.utils import fixup_filterset_query_params
876
874
 
877
875
  sort_order = []
878
876
  per_page = None
@@ -889,6 +887,8 @@ def saved_view_modal(
889
887
  "table_changes_pending",
890
888
  "clear_view",
891
889
  ]
890
+ param_dict = {}
891
+ filters_applied = fixup_filterset_query_params(parse_qs(params), view, non_filter_params)
892
892
 
893
893
  view_class = lookup.get_view_for_model(model, "List")
894
894
  table_name = None
@@ -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,72 @@ 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"]')
88
+
89
+ def click_table_link(self, row=1, column=2):
90
+ """By default, tries to click column next to checkbox to go to the details page."""
91
+ self.browser.find_by_xpath(f'//*[@id="object_list_form"]//tbody/tr[{row}]/td[{column}]/a').click()
44
92
 
45
93
  @property
46
94
  def objects_list_visible_items(self):
47
- objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]/div[1]/div')
95
+ """
96
+ Calculating the visible items. Return 0 if there is no visible items.
97
+ """
98
+ objects_table_container = self.browser.find_by_xpath('//*[@id="object_list_form"]')
48
99
  try:
49
100
  objects_table = objects_table_container.find_by_tag("tbody")
50
101
  return len(objects_table.find_by_tag("tr"))
@@ -52,19 +103,48 @@ class ObjectsListMixin:
52
103
  return 0
53
104
 
54
105
  def apply_filter(self, field, value):
106
+ """
107
+ Open filter dialog and apply select2 filters.
108
+ You can apply more values to the same filter, by calling this function with same name but different value.
109
+ """
55
110
  self.browser.find_by_xpath('//*[@id="id__filterbtn"]').click()
56
111
  self.fill_filters_select2_field(field, value)
57
- self.browser.find_by_xpath('//*[@id="default-filter"]//button[@type="submit"]').click()
112
+ self.click_button('#default-filter button[type="submit"]')
113
+
114
+
115
+ class ObjectDetailsMixin:
116
+ def assertPanelValue(self, panel_label, field_label, expected_value, exact_match=False):
117
+ """
118
+ Find the proper panel and asserts if given value match rendered field value.
119
+ By default, it's not using the exact match, because on the UI we're often adding
120
+ additional tags, relationships or units.
121
+ """
122
+ panel_xpath = f'//*[@id="main"]//div[@class="panel-heading"][contains(normalize-space(), "{panel_label}")]/following-sibling::table'
123
+ value = self.browser.find_by_xpath(f'{panel_xpath}//td[text()="{field_label}"]/following-sibling::td[1]').text
124
+
125
+ if exact_match:
126
+ self.assertEqual(value, str(expected_value))
127
+ else:
128
+ self.assertIn(str(expected_value), value)
58
129
 
59
130
 
60
131
  class BulkOperationsMixin:
61
132
  def confirm_bulk_delete_operation(self):
62
- self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').click()
133
+ """
134
+ Confirms bulk delete operation on the "warning" page after clicking bulk delete buttons.
135
+ """
136
+ self.click_button('button[name="_confirm"][type="submit"]')
63
137
 
64
138
  def submit_bulk_edit_operation(self):
65
- self.browser.find_by_xpath("//button[@name='_apply']", wait_time=5).click()
139
+ """
140
+ Submits the bulk edit form.
141
+ """
142
+ self.click_button('button[name="_apply"]')
66
143
 
67
144
  def wait_for_job_result(self):
145
+ """
146
+ Waits 30s for job to be finished.
147
+ """
68
148
  end_statuses = ["Completed", "Failed"]
69
149
  WebDriverWait(self.browser, 30).until(
70
150
  lambda driver: driver.find_by_id("pending-result-label").text in end_statuses
@@ -73,16 +153,52 @@ class BulkOperationsMixin:
73
153
  return self.browser.find_by_id("pending-result-label").text
74
154
 
75
155
  def verify_job_description(self, expected_job_description):
156
+ """
157
+ Verifies if the job description is correct.
158
+ Waits 30s on page load in case of large payload being sent from bulk edit form.
159
+ """
160
+ WebDriverWait(self.browser, 30).until(lambda driver: driver.is_text_present("Job Description"))
161
+
76
162
  job_description = self.browser.find_by_xpath('//td[text()="Job Description"]/following-sibling::td[1]').text
77
163
  self.assertEqual(job_description, expected_job_description)
78
164
 
165
+ def update_edit_form_value(self, field_name, value, is_select=False):
166
+ """
167
+ Updates bulk edit form value.
168
+ """
169
+ if is_select:
170
+ self.fill_select2_field(field_name, value)
171
+ else:
172
+ self.browser.fill(field_name, value)
173
+
174
+ def assertBulkDeleteConfirmMessageIsValid(self, expected_count):
175
+ """
176
+ Asserts that bulk delete confirmation message is valid and if we're deleting proper number of items.
177
+ """
178
+ self.browser.is_element_present_by_tag("body", wait_time=30)
179
+
180
+ button_text = self.browser.find_by_xpath('//button[@name="_confirm" and @type="submit"]').text
181
+ self.assertIn(f"Delete these {expected_count}", button_text)
182
+
183
+ message_text = self.browser.find_by_id("confirm-bulk-deletion").find_by_xpath('//div[@class="panel-body"]').text
184
+ self.assertIn(f"The following operation will delete {expected_count}", message_text)
185
+
79
186
  def assertIsBulkDeleteJob(self):
187
+ """
188
+ Asserts if currently visible job is bulk delete job.
189
+ """
80
190
  self.verify_job_description("Bulk delete objects.")
81
191
 
82
192
  def assertIsBulkEditJob(self):
193
+ """
194
+ Asserts if currently visible job is bulk edit job.
195
+ """
83
196
  self.verify_job_description("Bulk edit objects.")
84
197
 
85
198
  def assertJobStatusIsCompleted(self):
199
+ """
200
+ Asserts that job was successfully completed.
201
+ """
86
202
  job_status = self.wait_for_job_result()
87
203
  self.assertEqual(job_status, "Completed")
88
204
 
@@ -100,6 +216,7 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
100
216
 
101
217
  host = "0.0.0.0" # noqa: S104 # hardcoded-bind-all-interfaces -- false positive
102
218
  selenium_host = SELENIUM_HOST # Docker: `nautobot`; else `host.docker.internal`
219
+ logged_in = False
103
220
 
104
221
  @classmethod
105
222
  def setUpClass(cls):
@@ -123,6 +240,10 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
123
240
  def live_server_url(cls): # pylint: disable=no-self-argument
124
241
  return f"http://{cls.selenium_host}:{cls.server_thread.port}"
125
242
 
243
+ def tearDown(self):
244
+ if self.logged_in:
245
+ self.logout()
246
+
126
247
  @classmethod
127
248
  def tearDownClass(cls):
128
249
  """Close down the browser after tests are ran."""
@@ -195,19 +316,27 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
195
316
  self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
196
317
  return search_box
197
318
 
319
+ def _select_select2_result(self):
320
+ found_results = self.browser.find_by_css(".select2-results li.select2-results__option")
321
+ # click the first found item if it's not `None`: special value to nullify field
322
+ if found_results.first.text != "None":
323
+ found_results.first.click()
324
+ else:
325
+ found_results[1].click()
326
+
198
327
  def fill_select2_field(self, field_name, value):
199
328
  """
200
329
  Helper function to fill a Select2 single selection field on add/edit forms.
201
330
  """
202
- search_box = self._fill_select2_field(field_name, value)
203
- search_box.first.type(Keys.ENTER)
331
+ self._fill_select2_field(field_name, value)
332
+ self._select_select2_result()
204
333
 
205
334
  def fill_filters_select2_field(self, field_name, value):
206
335
  """
207
336
  Helper function to fill a Select2 single selection field on filters modals.
208
337
  """
209
338
  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()
339
+ self._select_select2_result()
211
340
 
212
341
  def fill_select2_multiselect_field(self, field_name, value):
213
342
  """
@@ -220,3 +349,331 @@ class SeleniumTestCase(StaticLiveServerTestCase, testing.NautobotTestCaseMixin):
220
349
  # wait for "searching" to disappear
221
350
  self.browser.is_element_not_present_by_css(".loading-results", wait_time=5)
222
351
  search_box.first.type(Keys.ENTER)
352
+
353
+ def click_button(self, query_selector):
354
+ btn = self.browser.find_by_css(query_selector, wait_time=5)
355
+ # Button might be visible but on the edge and then impossible to click due to vertical/horizontal scrolls
356
+ self.browser.execute_script(f"document.querySelector('{query_selector}').scrollIntoView()")
357
+ btn.click()
358
+
359
+ def login_as_superuser(self):
360
+ self.user.is_superuser = True
361
+ self.user.save()
362
+ self.login(self.user.username, self.password)
363
+ self.logged_in = True
364
+
365
+
366
+ class BulkOperationsTestCases:
367
+ """
368
+ Helper classes that runs all the basic bulk-operations test cases like edit/delete with
369
+ filtered / not filtered items along with select all option.
370
+
371
+ To use this class create required items in setUp method:
372
+ - at least four entities in two different groups,
373
+ - provide field for filtering to distinguish between above two groups
374
+ - provide expected counts (if different from default)
375
+ - set edit field and value
376
+ """
377
+
378
+ class BaseTestCase(SeleniumTestCase):
379
+ model_menu_path: tuple[str, str]
380
+ model_base_viewname: str
381
+ model_edit_data: dict[str, Any]
382
+ model_filter_by: dict[str, Any]
383
+ model_class: type[Model]
384
+ override_model_plural: Optional[str] = None
385
+ model_expected_counts: dict[str, int] = {
386
+ "all": 5,
387
+ "filtered": 2,
388
+ }
389
+
390
+ @property
391
+ def model_plural(self) -> str:
392
+ if self.override_model_plural is None:
393
+ return self.model_class._meta.verbose_name_plural
394
+
395
+ return self.override_model_plural
396
+
397
+ def setUp(self):
398
+ super().setUp()
399
+
400
+ self.setup_items()
401
+ self.login_as_superuser()
402
+ self.go_to_model_list_page()
403
+
404
+ def go_to_model_list_page(self):
405
+ self.click_navbar_entry(*self.model_menu_path)
406
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
407
+
408
+ def setup_items(self):
409
+ raise NotImplementedError
410
+
411
+ class BulkEditTestCase(BaseTestCase, ObjectsListMixin, BulkOperationsMixin):
412
+ def test_bulk_edit_require_selection(self):
413
+ # Click "edit selected" without selecting anything
414
+ self.click_bulk_edit()
415
+
416
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
417
+ self.assertTrue(self.browser.is_text_present(f"No {self.model_plural} were selected", wait_time=5))
418
+
419
+ def test_bulk_edit_all_items(self):
420
+ # Select all items and edit them
421
+ self.select_all_items()
422
+ self.click_bulk_edit()
423
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"))
424
+
425
+ # Edit some data and submit the form
426
+ for field_name, field_value in self.model_edit_data.items():
427
+ self.update_edit_form_value(field_name, field_value)
428
+ self.submit_bulk_edit_operation()
429
+
430
+ # Verify job output
431
+ self.assertIsBulkEditJob()
432
+ self.assertJobStatusIsCompleted()
433
+
434
+ # Assert that data was changed
435
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
436
+ self.assertEqual(found_items, self.model_expected_counts["all"])
437
+
438
+ def test_bulk_edit_one_item(self):
439
+ # Select one filtered item
440
+ self.select_one_item()
441
+ self.click_bulk_edit()
442
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"))
443
+
444
+ # Edit some data and submit the form
445
+ for field_name, field_value in self.model_edit_data.items():
446
+ self.update_edit_form_value(field_name, field_value)
447
+ self.submit_bulk_edit_operation()
448
+
449
+ # Verify job output
450
+ self.assertIsBulkEditJob()
451
+ self.assertJobStatusIsCompleted()
452
+
453
+ # Assert that data was changed
454
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
455
+ self.assertEqual(found_items, 1)
456
+
457
+ def test_bulk_edit_all_items_from_all_pages(self):
458
+ # Select all from all pages
459
+ self.set_per_page()
460
+ self.select_all_items_from_all_pages()
461
+ self.click_bulk_edit_all()
462
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"), self.browser.url)
463
+
464
+ # Edit some data and submit the form
465
+ for field_name, field_value in self.model_edit_data.items():
466
+ self.update_edit_form_value(field_name, field_value)
467
+ self.submit_bulk_edit_operation()
468
+
469
+ # Verify job output
470
+ self.assertIsBulkEditJob()
471
+ self.assertJobStatusIsCompleted()
472
+
473
+ # Assert that data was changed
474
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
475
+ self.assertEqual(found_items, self.model_expected_counts["all"])
476
+
477
+ def test_bulk_edit_all_filtered_items(self):
478
+ # Filter items
479
+ for field, value in self.model_filter_by.items():
480
+ self.apply_filter(field, value)
481
+
482
+ # Select all filtered items
483
+ self.select_all_items()
484
+ self.click_bulk_edit()
485
+ self.assertIn(
486
+ self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"),
487
+ self.browser.url,
488
+ )
489
+
490
+ # Edit some data and submit the form
491
+ for field_name, field_value in self.model_edit_data.items():
492
+ self.update_edit_form_value(field_name, field_value)
493
+ self.submit_bulk_edit_operation()
494
+
495
+ # Verify job output
496
+ self.assertIsBulkEditJob()
497
+ self.assertJobStatusIsCompleted()
498
+
499
+ # Assert that data was changed
500
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
501
+ self.assertEqual(found_items, self.model_expected_counts["filtered"])
502
+
503
+ def test_bulk_edit_one_filtered_item(self):
504
+ # Filter items
505
+ for field, value in self.model_filter_by.items():
506
+ self.apply_filter(field, value)
507
+
508
+ # Select one item and edit it
509
+ self.select_one_item()
510
+ self.click_bulk_edit()
511
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"), self.browser.url)
512
+
513
+ # Edit some data and submit the form
514
+ for field_name, field_value in self.model_edit_data.items():
515
+ self.update_edit_form_value(field_name, field_value)
516
+ self.submit_bulk_edit_operation()
517
+
518
+ # Verify job output
519
+ self.assertIsBulkEditJob()
520
+ self.assertJobStatusIsCompleted()
521
+
522
+ # Assert that data was changed
523
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
524
+ self.assertEqual(found_items, 1)
525
+
526
+ def test_bulk_edit_all_filtered_items_from_all_pages(self):
527
+ # Filter items
528
+ self.set_per_page()
529
+ for field, value in self.model_filter_by.items():
530
+ self.apply_filter(field, value)
531
+
532
+ # Select all items and delete them
533
+ self.select_all_items_from_all_pages()
534
+ self.click_bulk_edit_all()
535
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_edit"), self.browser.url)
536
+
537
+ # Edit some data and submit the form
538
+ for field_name, field_value in self.model_edit_data.items():
539
+ self.update_edit_form_value(field_name, field_value)
540
+ self.submit_bulk_edit_operation()
541
+
542
+ # Verify job output
543
+ self.assertIsBulkEditJob()
544
+ self.assertJobStatusIsCompleted()
545
+
546
+ # Assert that data was changed
547
+ found_items = self.model_class.objects.filter(**self.model_edit_data).count()
548
+ self.assertEqual(found_items, self.model_expected_counts["filtered"])
549
+
550
+ class BulkDeleteTestCase(BaseTestCase, ObjectsListMixin, BulkOperationsMixin):
551
+ def test_bulk_delete_require_selection(self):
552
+ # Click "delete selected" without selecting anything
553
+ self.click_bulk_delete()
554
+
555
+ self.assertEqual(self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_list"))
556
+ self.assertTrue(
557
+ self.browser.is_text_present(f"No {self.model_plural} were selected for deletion.", wait_time=5)
558
+ )
559
+
560
+ def test_bulk_delete_all_items(self):
561
+ # Select all items and delete them
562
+ self.select_all_items()
563
+ self.click_bulk_delete()
564
+
565
+ self.assertEqual(
566
+ self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete")
567
+ )
568
+ self.assertBulkDeleteConfirmMessageIsValid(self.model_expected_counts["all"])
569
+ self.confirm_bulk_delete_operation()
570
+
571
+ # Verify job output
572
+ self.assertIsBulkDeleteJob()
573
+ self.assertJobStatusIsCompleted()
574
+
575
+ self.go_to_model_list_page()
576
+ self.assertEqual(self.objects_list_visible_items, 0)
577
+
578
+ def test_bulk_delete_one_item(self):
579
+ # Select one item and delete it
580
+ self.select_one_item()
581
+ self.click_bulk_delete()
582
+
583
+ self.assertEqual(
584
+ self.browser.url, self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete")
585
+ )
586
+ self.assertBulkDeleteConfirmMessageIsValid(1)
587
+ self.confirm_bulk_delete_operation()
588
+
589
+ # Verify job output
590
+ self.assertIsBulkDeleteJob()
591
+ self.assertJobStatusIsCompleted()
592
+
593
+ self.go_to_model_list_page()
594
+ self.assertEqual(self.objects_list_visible_items, self.model_expected_counts["all"] - 1)
595
+
596
+ def test_bulk_delete_all_items_from_all_pages(self):
597
+ # Select all from all pages
598
+ self.set_per_page()
599
+ self.select_all_items_from_all_pages()
600
+ self.click_bulk_delete_all()
601
+
602
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
603
+ self.assertBulkDeleteConfirmMessageIsValid(self.model_expected_counts["all"])
604
+ self.confirm_bulk_delete_operation()
605
+
606
+ # Verify job output
607
+ self.assertIsBulkDeleteJob()
608
+ self.assertJobStatusIsCompleted()
609
+
610
+ self.go_to_model_list_page()
611
+ self.assertEqual(self.objects_list_visible_items, 0)
612
+
613
+ def test_bulk_delete_all_filtered_items(self):
614
+ # Filter items
615
+ for field, value in self.model_filter_by.items():
616
+ self.apply_filter(field, value)
617
+
618
+ # Select all items and delete them
619
+ self.select_all_items()
620
+ self.click_bulk_delete()
621
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
622
+ self.confirm_bulk_delete_operation()
623
+
624
+ # Verify job output
625
+ self.assertIsBulkDeleteJob()
626
+ self.assertJobStatusIsCompleted()
627
+
628
+ self.go_to_model_list_page()
629
+ rest_items_count = self.model_expected_counts["all"] - self.model_expected_counts["filtered"]
630
+ self.assertEqual(self.objects_list_visible_items, rest_items_count)
631
+
632
+ def test_bulk_delete_one_filtered_items(self):
633
+ # Filter items
634
+ for field, value in self.model_filter_by.items():
635
+ self.apply_filter(field, value)
636
+
637
+ # Select one item and delete it
638
+ self.select_one_item()
639
+ self.click_bulk_delete()
640
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
641
+ self.confirm_bulk_delete_operation()
642
+
643
+ # Verify job output
644
+ self.assertIsBulkDeleteJob()
645
+ self.assertJobStatusIsCompleted()
646
+
647
+ self.go_to_model_list_page()
648
+ self.assertEqual(self.objects_list_visible_items, self.model_expected_counts["all"] - 1)
649
+
650
+ def test_bulk_delete_all_filtered_items_from_all_pages(self):
651
+ # Filter items
652
+ self.set_per_page()
653
+ for field, value in self.model_filter_by.items():
654
+ self.apply_filter(field, value)
655
+
656
+ # Select all items and delete them
657
+ self.select_all_items_from_all_pages()
658
+ self.click_bulk_delete_all()
659
+
660
+ self.assertIn(self.live_server_url + reverse(f"{self.model_base_viewname}_bulk_delete"), self.browser.url)
661
+ self.assertBulkDeleteConfirmMessageIsValid(self.model_expected_counts["filtered"])
662
+ self.confirm_bulk_delete_operation()
663
+
664
+ # Verify job output
665
+ self.assertIsBulkDeleteJob()
666
+ self.assertJobStatusIsCompleted()
667
+
668
+ self.go_to_model_list_page()
669
+ self.set_per_page(50) # Set page size back to default
670
+ rest_items_count = self.model_expected_counts["all"] - self.model_expected_counts["filtered"]
671
+ self.assertEqual(self.objects_list_visible_items, rest_items_count)
672
+
673
+ # Filter again and assert that all items were deleted
674
+ for field, value in self.model_filter_by.items():
675
+ self.apply_filter(field, value)
676
+ self.assertEqual(self.objects_list_visible_items, 0)
677
+
678
+ class BulkOperationsTestCase(BulkEditTestCase, BulkDeleteTestCase):
679
+ pass
@@ -0,0 +1,31 @@
1
+ from io import StringIO
2
+
3
+ from django.core.management import call_command
4
+ import yaml
5
+
6
+ from nautobot.core.testing import TestCase
7
+
8
+
9
+ class ManagementCommandTestCase(TestCase):
10
+ """Test case for core management commands."""
11
+
12
+ def setUp(self):
13
+ """Initialize user and client."""
14
+ super().setUpNautobot()
15
+ self.user.is_superuser = True
16
+ self.user.is_staff = True
17
+ self.user.save()
18
+ self.client.force_login(self.user)
19
+
20
+ def test_generate_performance_test_endpoints(self):
21
+ """Test the generate_performance_test_endpoints management command."""
22
+ out = StringIO()
23
+ call_command("generate_performance_test_endpoints", stdout=out)
24
+ endpoints_dict = yaml.safe_load(out.getvalue())["endpoints"]
25
+ # status_code_to_endpoints = collections.defaultdict(list)
26
+ for view_name, value in endpoints_dict.items():
27
+ for endpoint in value:
28
+ response = self.client.get(endpoint, follow=True)
29
+ self.assertHttpStatus(
30
+ response, 200, f"{view_name}: {endpoint} returns status Code {response.status_code} instead of 200"
31
+ )