nautobot 2.2.2__py3-none-any.whl → 2.2.4__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 (362) hide show
  1. nautobot/apps/jobs.py +2 -0
  2. nautobot/core/api/utils.py +12 -9
  3. nautobot/core/apps/__init__.py +2 -2
  4. nautobot/core/celery/__init__.py +79 -68
  5. nautobot/core/celery/backends.py +9 -1
  6. nautobot/core/celery/control.py +4 -7
  7. nautobot/core/celery/schedulers.py +4 -2
  8. nautobot/core/celery/task.py +78 -5
  9. nautobot/core/graphql/schema.py +2 -1
  10. nautobot/core/jobs/__init__.py +2 -1
  11. nautobot/core/settings.py +6 -4
  12. nautobot/core/settings.yaml +51 -16
  13. nautobot/core/templates/admin/base.html +2 -2
  14. nautobot/core/templates/base_django.html +2 -2
  15. nautobot/core/templates/buttons/export.html +47 -47
  16. nautobot/core/templates/generic/object_list.html +3 -3
  17. nautobot/core/templates/inc/javascript.html +3 -0
  18. nautobot/core/templates/inc/media.html +3 -0
  19. nautobot/core/templates/login.html +2 -2
  20. nautobot/core/templates/nautobot_config.py.j2 +2 -0
  21. nautobot/core/templatetags/helpers.py +66 -9
  22. nautobot/core/testing/__init__.py +6 -1
  23. nautobot/core/testing/api.py +12 -13
  24. nautobot/core/testing/mixins.py +2 -2
  25. nautobot/core/testing/views.py +50 -51
  26. nautobot/core/tests/test_api.py +23 -2
  27. nautobot/core/tests/test_jobs.py +79 -2
  28. nautobot/core/tests/test_templatetags_helpers.py +32 -0
  29. nautobot/core/tests/test_views.py +52 -0
  30. nautobot/core/tests/test_views_utils.py +22 -1
  31. nautobot/core/utils/module_loading.py +89 -0
  32. nautobot/core/views/mixins.py +4 -0
  33. nautobot/core/views/utils.py +3 -2
  34. nautobot/dcim/choices.py +14 -0
  35. nautobot/dcim/forms.py +51 -1
  36. nautobot/dcim/models/device_components.py +9 -5
  37. nautobot/dcim/templates/dcim/location.html +32 -13
  38. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +102 -0
  39. nautobot/dcim/tests/test_views.py +376 -55
  40. nautobot/dcim/urls.py +5 -0
  41. nautobot/dcim/views.py +172 -21
  42. nautobot/extras/api/serializers.py +17 -6
  43. nautobot/extras/api/views.py +21 -10
  44. nautobot/extras/constants.py +3 -3
  45. nautobot/extras/datasources/git.py +47 -58
  46. nautobot/extras/forms/forms.py +3 -1
  47. nautobot/extras/jobs.py +79 -146
  48. nautobot/extras/models/datasources.py +0 -2
  49. nautobot/extras/models/jobs.py +36 -18
  50. nautobot/extras/plugins/__init__.py +1 -20
  51. nautobot/extras/signals.py +6 -9
  52. nautobot/extras/test_jobs/__init__.py +8 -0
  53. nautobot/extras/test_jobs/dry_run.py +3 -2
  54. nautobot/extras/test_jobs/fail.py +43 -0
  55. nautobot/extras/test_jobs/ipaddress_vars.py +40 -1
  56. nautobot/extras/test_jobs/jobs_module/__init__.py +5 -0
  57. nautobot/extras/test_jobs/jobs_module/jobs_submodule/__init__.py +1 -0
  58. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +6 -0
  59. nautobot/extras/test_jobs/pass.py +40 -0
  60. nautobot/extras/test_jobs/relative_import.py +11 -0
  61. nautobot/extras/tests/test_api.py +3 -0
  62. nautobot/extras/tests/test_context_managers.py +18 -0
  63. nautobot/extras/tests/test_datasources.py +125 -118
  64. nautobot/extras/tests/test_job_variables.py +57 -15
  65. nautobot/extras/tests/test_jobs.py +135 -1
  66. nautobot/extras/tests/test_models.py +26 -19
  67. nautobot/extras/tests/test_plugins.py +1 -3
  68. nautobot/extras/tests/test_views.py +2 -4
  69. nautobot/extras/utils.py +2 -1
  70. nautobot/extras/views.py +82 -116
  71. nautobot/ipam/api/views.py +8 -1
  72. nautobot/ipam/graphql/types.py +11 -0
  73. nautobot/ipam/mixins.py +32 -0
  74. nautobot/ipam/models.py +2 -1
  75. nautobot/ipam/querysets.py +6 -1
  76. nautobot/ipam/tests/test_models.py +82 -0
  77. nautobot/ipam/views.py +6 -6
  78. nautobot/project-static/docs/404.html +107 -51
  79. nautobot/project-static/docs/apps/index.html +107 -51
  80. nautobot/project-static/docs/apps/nautobot-apps.html +107 -51
  81. nautobot/project-static/docs/assets/_mkdocstrings.css +6 -1
  82. nautobot/project-static/docs/assets/extra.css +11 -0
  83. nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js +29 -0
  84. nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js.map +7 -0
  85. nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css +1 -0
  86. nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css.map +1 -0
  87. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +107 -51
  88. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +107 -51
  89. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +108 -52
  90. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +107 -51
  91. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +107 -51
  92. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +107 -51
  93. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +107 -51
  94. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +107 -51
  95. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +107 -51
  96. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +107 -51
  97. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +107 -51
  98. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +107 -51
  99. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +107 -51
  100. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +287 -262
  101. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +107 -51
  102. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +107 -51
  103. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +107 -51
  104. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +107 -51
  105. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +107 -51
  106. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +107 -51
  107. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +107 -51
  108. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +107 -51
  109. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +107 -51
  110. nautobot/project-static/docs/development/apps/api/configuration-view.html +110 -54
  111. nautobot/project-static/docs/development/apps/api/database-backend-config.html +110 -54
  112. nautobot/project-static/docs/development/apps/api/models/django-admin.html +107 -51
  113. nautobot/project-static/docs/development/apps/api/models/global-search.html +110 -54
  114. nautobot/project-static/docs/development/apps/api/models/graphql.html +113 -57
  115. nautobot/project-static/docs/development/apps/api/models/index.html +107 -51
  116. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +113 -57
  117. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +107 -51
  118. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +110 -54
  119. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +107 -51
  120. nautobot/project-static/docs/development/apps/api/platform-features/index.html +107 -51
  121. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +110 -54
  122. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +111 -55
  123. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +110 -54
  124. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +110 -54
  125. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +107 -51
  126. nautobot/project-static/docs/development/apps/api/prometheus.html +110 -54
  127. nautobot/project-static/docs/development/apps/api/setup.html +107 -51
  128. nautobot/project-static/docs/development/apps/api/testing.html +113 -57
  129. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +110 -54
  130. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +110 -54
  131. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +107 -51
  132. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +107 -51
  133. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +113 -57
  134. nautobot/project-static/docs/development/apps/api/views/base-template.html +107 -51
  135. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +110 -54
  136. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +107 -51
  137. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +110 -54
  138. nautobot/project-static/docs/development/apps/api/views/index.html +107 -51
  139. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +113 -57
  140. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +122 -66
  141. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +110 -54
  142. nautobot/project-static/docs/development/apps/api/views/notes.html +110 -54
  143. nautobot/project-static/docs/development/apps/api/views/rest-api.html +107 -51
  144. nautobot/project-static/docs/development/apps/api/views/urls.html +107 -51
  145. nautobot/project-static/docs/development/apps/index.html +128 -72
  146. nautobot/project-static/docs/development/apps/migration/code-updates.html +107 -51
  147. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +107 -51
  148. nautobot/project-static/docs/development/apps/migration/from-v1.html +107 -51
  149. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +107 -51
  150. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +107 -51
  151. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +107 -51
  152. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +107 -51
  153. nautobot/project-static/docs/development/apps/porting-from-netbox.html +110 -54
  154. nautobot/project-static/docs/development/core/application-registry.html +242 -144
  155. nautobot/project-static/docs/development/core/best-practices.html +122 -66
  156. nautobot/project-static/docs/development/core/bootstrap-ui.html +107 -51
  157. nautobot/project-static/docs/development/core/caching.html +107 -51
  158. nautobot/project-static/docs/development/core/controllers.html +107 -51
  159. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +113 -57
  160. nautobot/project-static/docs/development/core/generic-views.html +110 -54
  161. nautobot/project-static/docs/development/core/getting-started.html +135 -79
  162. nautobot/project-static/docs/development/core/homepage.html +110 -54
  163. nautobot/project-static/docs/development/core/index.html +107 -51
  164. nautobot/project-static/docs/development/core/model-checklist.html +156 -52
  165. nautobot/project-static/docs/development/core/model-features.html +108 -52
  166. nautobot/project-static/docs/development/core/natural-keys.html +110 -54
  167. nautobot/project-static/docs/development/core/navigation-menu.html +107 -51
  168. nautobot/project-static/docs/development/core/release-checklist.html +107 -51
  169. nautobot/project-static/docs/development/core/role-internals.html +107 -51
  170. nautobot/project-static/docs/development/core/settings.html +107 -51
  171. nautobot/project-static/docs/development/core/style-guide.html +110 -54
  172. nautobot/project-static/docs/development/core/templates.html +113 -57
  173. nautobot/project-static/docs/development/core/testing.html +125 -69
  174. nautobot/project-static/docs/development/core/user-preferences.html +107 -51
  175. nautobot/project-static/docs/development/index.html +107 -51
  176. nautobot/project-static/docs/development/jobs/index.html +504 -172
  177. nautobot/project-static/docs/development/jobs/migration/from-v1.html +111 -55
  178. nautobot/project-static/docs/docker/index.html +3 -3
  179. nautobot/project-static/docs/index.html +125 -69
  180. nautobot/project-static/docs/installation/selinux-troubleshooting.html +3 -3
  181. nautobot/project-static/docs/objects.inv +0 -0
  182. nautobot/project-static/docs/release-notes/index.html +107 -51
  183. nautobot/project-static/docs/release-notes/version-1.0.html +107 -51
  184. nautobot/project-static/docs/release-notes/version-1.1.html +107 -51
  185. nautobot/project-static/docs/release-notes/version-1.2.html +107 -51
  186. nautobot/project-static/docs/release-notes/version-1.3.html +107 -51
  187. nautobot/project-static/docs/release-notes/version-1.4.html +107 -51
  188. nautobot/project-static/docs/release-notes/version-1.5.html +116 -60
  189. nautobot/project-static/docs/release-notes/version-1.6.html +107 -51
  190. nautobot/project-static/docs/release-notes/version-2.0.html +110 -54
  191. nautobot/project-static/docs/release-notes/version-2.1.html +107 -51
  192. nautobot/project-static/docs/release-notes/version-2.2.html +500 -114
  193. nautobot/project-static/docs/requirements.txt +2 -2
  194. nautobot/project-static/docs/search/search_index.json +1 -1
  195. nautobot/project-static/docs/sitemap.xml +262 -262
  196. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  197. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +107 -51
  198. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +107 -51
  199. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +107 -51
  200. nautobot/project-static/docs/user-guide/administration/configuration/index.html +107 -51
  201. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +251 -164
  202. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +113 -57
  203. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +107 -51
  204. nautobot/project-static/docs/user-guide/administration/guides/caching.html +113 -57
  205. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +107 -51
  206. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +107 -51
  207. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +107 -51
  208. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +113 -57
  209. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +107 -51
  210. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +107 -51
  211. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +107 -51
  212. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +171 -112
  213. nautobot/project-static/docs/user-guide/administration/installation/docker.html +13 -8626
  214. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +117 -61
  215. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +13 -8614
  216. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +252 -165
  217. nautobot/project-static/docs/user-guide/administration/installation/index.html +165 -192
  218. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +411 -691
  219. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +248 -229
  220. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +13 -8118
  221. nautobot/project-static/docs/user-guide/administration/installation/services.html +350 -240
  222. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +8684 -0
  223. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +8672 -0
  224. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +8176 -0
  225. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +110 -54
  226. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +110 -54
  227. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +155 -99
  228. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +107 -51
  229. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +109 -53
  230. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +107 -51
  231. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +107 -51
  232. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +107 -51
  233. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +107 -51
  234. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +107 -51
  235. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +107 -51
  236. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +117 -58
  237. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +113 -57
  238. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +107 -51
  239. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +107 -51
  240. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +107 -51
  241. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +107 -51
  242. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +110 -54
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +107 -51
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +110 -54
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +110 -54
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +110 -54
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +110 -54
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +107 -51
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +107 -51
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +113 -57
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +110 -54
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +110 -54
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +110 -54
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +110 -54
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +116 -60
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +110 -54
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +110 -54
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +119 -63
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +110 -54
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +110 -54
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +113 -57
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +113 -57
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +113 -57
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +107 -51
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +113 -57
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +107 -51
  267. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +110 -54
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +110 -54
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +107 -51
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +110 -54
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +110 -54
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +107 -51
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +107 -51
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +107 -51
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +110 -54
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +110 -54
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +110 -54
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +110 -54
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +107 -51
  280. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +113 -57
  281. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +110 -54
  282. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +110 -54
  283. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +110 -54
  284. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +125 -69
  285. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +113 -57
  286. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +128 -72
  287. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +110 -54
  288. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +107 -51
  289. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +110 -54
  290. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +227 -60
  291. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +107 -51
  292. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +113 -57
  293. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +107 -51
  294. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +110 -54
  295. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +107 -51
  296. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +107 -51
  297. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +107 -51
  298. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +107 -51
  299. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +113 -57
  300. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +113 -57
  301. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +107 -51
  302. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +113 -57
  303. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +107 -51
  304. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +107 -51
  305. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +107 -51
  306. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +107 -51
  307. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +107 -51
  308. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +107 -51
  309. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +107 -51
  310. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +107 -51
  311. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +107 -51
  312. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +110 -54
  313. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +113 -57
  314. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +107 -51
  315. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +110 -54
  316. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +107 -51
  317. nautobot/project-static/docs/user-guide/index.html +109 -53
  318. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +107 -51
  319. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +113 -57
  320. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +128 -72
  321. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +107 -51
  322. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +125 -69
  323. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +107 -51
  324. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +110 -54
  325. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +125 -69
  326. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +110 -54
  327. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +107 -51
  328. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +107 -51
  329. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +143 -100
  330. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +113 -57
  331. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +113 -57
  332. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +110 -54
  333. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +123 -67
  334. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +107 -51
  335. nautobot/project-static/docs/user-guide/platform-functionality/note.html +110 -54
  336. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +116 -60
  337. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +110 -54
  338. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +131 -75
  339. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +149 -93
  340. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +110 -54
  341. nautobot/project-static/docs/user-guide/platform-functionality/role.html +107 -51
  342. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +116 -60
  343. nautobot/project-static/docs/user-guide/platform-functionality/status.html +119 -63
  344. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +110 -54
  345. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +137 -81
  346. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +110 -54
  347. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +107 -51
  348. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +110 -54
  349. nautobot/project-static/js/forms.js +18 -11
  350. nautobot/tenancy/views.py +2 -6
  351. nautobot/virtualization/views.py +5 -9
  352. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/METADATA +4 -4
  353. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/RECORD +357 -348
  354. nautobot/extras/test_jobs/job_variables.py +0 -93
  355. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js +0 -29
  356. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js.map +0 -7
  357. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +0 -1
  358. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +0 -1
  359. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/LICENSE.txt +0 -0
  360. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/NOTICE +0 -0
  361. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/WHEEL +0 -0
  362. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/entry_points.txt +0 -0
@@ -15,11 +15,10 @@ from django.utils.http import urlencode
15
15
  from django.utils.text import slugify
16
16
  from tree_queries.models import TreeNode
17
17
 
18
- from nautobot.core import testing
19
18
  from nautobot.core.models.generics import PrimaryModel
20
19
  from nautobot.core.models.tree_queries import TreeModel
21
20
  from nautobot.core.templatetags import helpers
22
- from nautobot.core.testing import mixins
21
+ from nautobot.core.testing import mixins, utils
23
22
  from nautobot.core.utils import lookup
24
23
  from nautobot.extras import choices as extras_choices, models as extras_models, querysets as extras_querysets
25
24
  from nautobot.extras.forms import CustomFieldModelFormMixin, RelationshipModelFormMixin
@@ -152,7 +151,7 @@ class ViewTestCases:
152
151
 
153
152
  # The "Change Log" tab should appear in the response since we have all exempt permissions
154
153
  if issubclass(self.model, extras_models.ChangeLoggedModel):
155
- response_body = testing.extract_page_body(response.content.decode(response.charset))
154
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
156
155
  self.assertIn("Change Log", response_body, msg=response_body)
157
156
 
158
157
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
@@ -160,7 +159,7 @@ class ViewTestCases:
160
159
  instance = self._get_queryset().first()
161
160
 
162
161
  # Try GET without permission
163
- with testing.disable_warnings("django.request"):
162
+ with utils.disable_warnings("django.request"):
164
163
  response = self.client.get(instance.get_absolute_url())
165
164
  self.assertHttpStatus(response, [403, 404])
166
165
  response_body = response.content.decode(response.charset)
@@ -180,7 +179,7 @@ class ViewTestCases:
180
179
  response = self.client.get(instance.get_absolute_url())
181
180
  self.assertHttpStatus(response, 200)
182
181
 
183
- response_body = testing.extract_page_body(response.content.decode(response.charset))
182
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
184
183
 
185
184
  # The object's display name or string representation should appear in the response
186
185
  self.assertIn(escape(getattr(instance, "display", str(instance))), response_body, msg=response_body)
@@ -247,7 +246,7 @@ class ViewTestCases:
247
246
  obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
248
247
 
249
248
  response = self.client.get(instance.get_absolute_url())
250
- response_body = testing.extract_page_body(response.content.decode(response.charset))
249
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
251
250
  advanced_tab_href = f"{instance.get_absolute_url()}#advanced"
252
251
 
253
252
  self.assertIn(advanced_tab_href, response_body)
@@ -304,16 +303,16 @@ class ViewTestCases:
304
303
 
305
304
  def test_create_object_without_permission(self):
306
305
  # Try GET without permission
307
- with testing.disable_warnings("django.request"):
306
+ with utils.disable_warnings("django.request"):
308
307
  self.assertHttpStatus(self.client.get(self._get_url("add")), 403)
309
308
 
310
309
  # Try POST without permission
311
310
  request = {
312
311
  "path": self._get_url("add"),
313
- "data": testing.post_data(self.form_data),
312
+ "data": utils.post_data(self.form_data),
314
313
  }
315
314
  response = self.client.post(**request)
316
- with testing.disable_warnings("django.request"):
315
+ with utils.disable_warnings("django.request"):
317
316
  self.assertHttpStatus(response, 403)
318
317
 
319
318
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -332,7 +331,7 @@ class ViewTestCases:
332
331
  # Try POST with model-level permission
333
332
  request = {
334
333
  "path": self._get_url("add"),
335
- "data": testing.post_data(self.form_data),
334
+ "data": utils.post_data(self.form_data),
336
335
  }
337
336
  self.assertHttpStatus(self.client.post(**request), 302)
338
337
  self.assertEqual(initial_count + 1, self._get_queryset().count())
@@ -362,7 +361,7 @@ class ViewTestCases:
362
361
  detail_url = instance.get_absolute_url()
363
362
  validate(detail_url)
364
363
  response = self.client.get(detail_url)
365
- response_body = testing.extract_page_body(response.content.decode(response.charset))
364
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
366
365
  advanced_tab_href = f"{detail_url}#advanced"
367
366
  self.assertIn(advanced_tab_href, response_body)
368
367
  self.assertIn("<td>Created By</td>", response_body)
@@ -391,7 +390,7 @@ class ViewTestCases:
391
390
  # Try to create an object (not permitted)
392
391
  request = {
393
392
  "path": self._get_url("add"),
394
- "data": testing.post_data(self.form_data),
393
+ "data": utils.post_data(self.form_data),
395
394
  }
396
395
  self.assertHttpStatus(self.client.post(**request), 200)
397
396
  self.assertEqual(initial_count, self._get_queryset().count()) # Check that no object was created
@@ -403,7 +402,7 @@ class ViewTestCases:
403
402
  # Try to create an object (permitted)
404
403
  request = {
405
404
  "path": self._get_url("add"),
406
- "data": testing.post_data(self.form_data),
405
+ "data": utils.post_data(self.form_data),
407
406
  }
408
407
  self.assertHttpStatus(self.client.post(**request), 302)
409
408
  self.assertEqual(initial_count + 1, self._get_queryset().count())
@@ -472,15 +471,15 @@ class ViewTestCases:
472
471
  instance = self._get_queryset().first()
473
472
 
474
473
  # Try GET without permission
475
- with testing.disable_warnings("django.request"):
474
+ with utils.disable_warnings("django.request"):
476
475
  self.assertHttpStatus(self.client.get(self._get_url("edit", instance)), [403, 404])
477
476
 
478
477
  # Try POST without permission
479
478
  request = {
480
479
  "path": self._get_url("edit", instance),
481
- "data": testing.post_data(self.form_data),
480
+ "data": utils.post_data(self.form_data),
482
481
  }
483
- with testing.disable_warnings("django.request"):
482
+ with utils.disable_warnings("django.request"):
484
483
  self.assertHttpStatus(self.client.post(**request), [403, 404])
485
484
 
486
485
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -499,7 +498,7 @@ class ViewTestCases:
499
498
  # Try POST with model-level permission
500
499
  request = {
501
500
  "path": self._get_url("edit", instance),
502
- "data": testing.post_data(self.form_data),
501
+ "data": utils.post_data(self.form_data),
503
502
  }
504
503
  self.assertHttpStatus(self.client.post(**request), 302)
505
504
  self.assertInstanceEqual(self._get_queryset().get(pk=instance.pk), self.form_data)
@@ -515,7 +514,7 @@ class ViewTestCases:
515
514
  detail_url = instance.get_absolute_url()
516
515
  validate(detail_url)
517
516
  response = self.client.get(detail_url)
518
- response_body = testing.extract_page_body(response.content.decode(response.charset))
517
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
519
518
  advanced_tab_href = f"{detail_url}#advanced"
520
519
  self.assertIn(advanced_tab_href, response_body)
521
520
  self.assertIn("<td>Last Updated By</td>", response_body)
@@ -547,7 +546,7 @@ class ViewTestCases:
547
546
  # Try to edit a permitted object
548
547
  request = {
549
548
  "path": self._get_url("edit", instance1),
550
- "data": testing.post_data(self.form_data),
549
+ "data": utils.post_data(self.form_data),
551
550
  }
552
551
  self.assertHttpStatus(self.client.post(**request), 302)
553
552
  self.assertInstanceEqual(self._get_queryset().get(pk=instance1.pk), self.form_data)
@@ -555,7 +554,7 @@ class ViewTestCases:
555
554
  # Try to edit a non-permitted object
556
555
  request = {
557
556
  "path": self._get_url("edit", instance2),
558
- "data": testing.post_data(self.form_data),
557
+ "data": utils.post_data(self.form_data),
559
558
  }
560
559
  self.assertHttpStatus(self.client.post(**request), 404)
561
560
 
@@ -571,7 +570,7 @@ class ViewTestCases:
571
570
  For some models this may just be any random object, but when we have FKs with `on_delete=models.PROTECT`
572
571
  (as is often the case) we need to find or create an instance that doesn't have such entanglements.
573
572
  """
574
- instance = testing.get_deletable_objects(self.model, self._get_queryset()).first()
573
+ instance = utils.get_deletable_objects(self.model, self._get_queryset()).first()
575
574
  if instance is None:
576
575
  self.fail("Couldn't find a single deletable object!")
577
576
  return instance
@@ -580,15 +579,15 @@ class ViewTestCases:
580
579
  instance = self.get_deletable_object()
581
580
 
582
581
  # Try GET without permission
583
- with testing.disable_warnings("django.request"):
582
+ with utils.disable_warnings("django.request"):
584
583
  self.assertHttpStatus(self.client.get(self._get_url("delete", instance)), [403, 404])
585
584
 
586
585
  # Try POST without permission
587
586
  request = {
588
587
  "path": self._get_url("delete", instance),
589
- "data": testing.post_data({"confirm": True}),
588
+ "data": utils.post_data({"confirm": True}),
590
589
  }
591
- with testing.disable_warnings("django.request"):
590
+ with utils.disable_warnings("django.request"):
592
591
  self.assertHttpStatus(self.client.post(**request), [403, 404])
593
592
 
594
593
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -624,7 +623,7 @@ class ViewTestCases:
624
623
  # Try POST with model-level permission
625
624
  request = {
626
625
  "path": self._get_url("delete", instance),
627
- "data": testing.post_data({"confirm": True}),
626
+ "data": utils.post_data({"confirm": True}),
628
627
  }
629
628
  self.assertHttpStatus(self.client.post(**request), 302)
630
629
  with self.assertRaises(ObjectDoesNotExist):
@@ -701,7 +700,7 @@ class ViewTestCases:
701
700
  # Try to delete a permitted object
702
701
  request = {
703
702
  "path": self._get_url("delete", instance1),
704
- "data": testing.post_data({"confirm": True}),
703
+ "data": utils.post_data({"confirm": True}),
705
704
  }
706
705
  self.assertHttpStatus(self.client.post(**request), 302)
707
706
  with self.assertRaises(ObjectDoesNotExist):
@@ -713,7 +712,7 @@ class ViewTestCases:
713
712
  instance3 = self._get_queryset().first()
714
713
  request = {
715
714
  "path": self._get_url("delete", instance3),
716
- "data": testing.post_data({"confirm": True}),
715
+ "data": utils.post_data({"confirm": True}),
717
716
  }
718
717
  self.assertHttpStatus(self.client.post(**request), 404)
719
718
  self.assertTrue(self._get_queryset().filter(pk=instance3.pk).exists())
@@ -781,7 +780,7 @@ class ViewTestCases:
781
780
  instance1, instance2 = self._get_queryset().all()[:2]
782
781
  response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
783
782
  self.assertHttpStatus(response, 200)
784
- content = testing.extract_page_body(response.content.decode(response.charset))
783
+ content = utils.extract_page_body(response.content.decode(response.charset))
785
784
  # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
786
785
  if hasattr(self.model, "name"):
787
786
  self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
@@ -794,7 +793,7 @@ class ViewTestCases:
794
793
  """Verify that with STRICT_FILTERING, an unknown filter results in an error message and no matches."""
795
794
  response = self.client.get(f"{self._get_url('list')}?ice_cream_flavor=chocolate")
796
795
  self.assertHttpStatus(response, 200)
797
- content = testing.extract_page_body(response.content.decode(response.charset))
796
+ content = utils.extract_page_body(response.content.decode(response.charset))
798
797
  # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
799
798
  self.assertIn("Unknown filter field", content, msg=content)
800
799
  # There should be no table rows displayed except for the empty results row
@@ -820,7 +819,7 @@ class ViewTestCases:
820
819
  ],
821
820
  )
822
821
  self.assertHttpStatus(response, 200)
823
- content = testing.extract_page_body(response.content.decode(response.charset))
822
+ content = utils.extract_page_body(response.content.decode(response.charset))
824
823
  # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
825
824
  self.assertNotIn("Unknown filter field", content, msg=content)
826
825
  self.assertIn("None", content, msg=content)
@@ -833,7 +832,7 @@ class ViewTestCases:
833
832
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
834
833
  def test_list_objects_without_permission(self):
835
834
  # Try GET without permission
836
- with testing.disable_warnings("django.request"):
835
+ with utils.disable_warnings("django.request"):
837
836
  response = self.client.get(self._get_url("list"))
838
837
  self.assertHttpStatus(response, 403)
839
838
  response_body = response.content.decode(response.charset)
@@ -884,7 +883,7 @@ class ViewTestCases:
884
883
  # Try GET with object-level permission
885
884
  response = self.client.get(self._get_url("list"))
886
885
  self.assertHttpStatus(response, 200)
887
- content = testing.extract_page_body(response.content.decode(response.charset))
886
+ content = utils.extract_page_body(response.content.decode(response.charset))
888
887
  # TODO: it'd make test failures more readable if we strip the page headers/footers from the content
889
888
  if hasattr(self.model, "name"):
890
889
  self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
@@ -952,11 +951,11 @@ class ViewTestCases:
952
951
  def test_create_multiple_objects_without_permission(self):
953
952
  request = {
954
953
  "path": self._get_url("add"),
955
- "data": testing.post_data(self.bulk_create_data),
954
+ "data": utils.post_data(self.bulk_create_data),
956
955
  }
957
956
 
958
957
  # Try POST without permission
959
- with testing.disable_warnings("django.request"):
958
+ with utils.disable_warnings("django.request"):
960
959
  self.assertHttpStatus(self.client.post(**request), 403)
961
960
 
962
961
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
@@ -964,7 +963,7 @@ class ViewTestCases:
964
963
  initial_count = self._get_queryset().count()
965
964
  request = {
966
965
  "path": self._get_url("add"),
967
- "data": testing.post_data(self.bulk_create_data),
966
+ "data": utils.post_data(self.bulk_create_data),
968
967
  }
969
968
 
970
969
  # Assign non-constrained permission
@@ -994,7 +993,7 @@ class ViewTestCases:
994
993
  initial_count = self._get_queryset().count()
995
994
  request = {
996
995
  "path": self._get_url("add"),
997
- "data": testing.post_data(self.bulk_create_data),
996
+ "data": utils.post_data(self.bulk_create_data),
998
997
  }
999
998
 
1000
999
  # Assign constrained permission
@@ -1078,7 +1077,7 @@ class ViewTestCases:
1078
1077
  }
1079
1078
 
1080
1079
  # Try POST without permission
1081
- with testing.disable_warnings("django.request"):
1080
+ with utils.disable_warnings("django.request"):
1082
1081
  self.assertHttpStatus(self.client.post(self._get_url("bulk_edit"), data), 403)
1083
1082
 
1084
1083
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -1090,7 +1089,7 @@ class ViewTestCases:
1090
1089
  }
1091
1090
 
1092
1091
  # Append the form data to the request
1093
- data.update(testing.post_data(self.bulk_edit_data))
1092
+ data.update(utils.post_data(self.bulk_edit_data))
1094
1093
 
1095
1094
  # Assign model-level permission
1096
1095
  obj_perm = users_models.ObjectPermission(name="Test permission", actions=["change"])
@@ -1124,7 +1123,7 @@ class ViewTestCases:
1124
1123
  # Expect a 200 status cause we are only rendering the bulk edit table.
1125
1124
  # after pressing Edit Selected button.
1126
1125
  self.assertHttpStatus(response, 200)
1127
- response_body = testing.extract_page_body(response.content.decode(response.charset))
1126
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
1128
1127
  # Check if all the pks are passed into the BulkEditForm/BulkUpdateForm
1129
1128
  for pk in pk_list:
1130
1129
  self.assertIn(f'<input type="hidden" name="pk" value="{pk}"', response_body)
@@ -1143,7 +1142,7 @@ class ViewTestCases:
1143
1142
  except StopIteration:
1144
1143
  self.fail(f"Test requires at least three instances of {self.model._meta.model_name} to be defined.")
1145
1144
 
1146
- post_data = testing.post_data(self.bulk_edit_data)
1145
+ post_data = utils.post_data(self.bulk_edit_data)
1147
1146
 
1148
1147
  # Open bulk update form with first two objects
1149
1148
  selected_data = {
@@ -1155,7 +1154,7 @@ class ViewTestCases:
1155
1154
  response = self.client.post(f"{self._get_url('bulk_edit')}?{query_string}", selected_data)
1156
1155
  # Expect a 200 status cause we are only rendering the bulk edit table after pressing Edit Selected button.
1157
1156
  self.assertHttpStatus(response, 200)
1158
- response_body = testing.extract_page_body(response.content.decode(response.charset))
1157
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
1159
1158
  # Check if the first and second pk is passed into the form.
1160
1159
  self.assertIn(f'<input type="hidden" name="pk" value="{first_pk}"', response_body)
1161
1160
  self.assertIn(f'<input type="hidden" name="pk" value="{second_pk}"', response_body)
@@ -1195,7 +1194,7 @@ class ViewTestCases:
1195
1194
  "pk": pk_list,
1196
1195
  "_apply": True, # Form button
1197
1196
  }
1198
- data.update(testing.post_data(self.bulk_edit_data))
1197
+ data.update(utils.post_data(self.bulk_edit_data))
1199
1198
 
1200
1199
  # Attempt to bulk edit permitted objects into a non-permitted state
1201
1200
  response = self.client.post(self._get_url("bulk_edit"), data)
@@ -1227,7 +1226,7 @@ class ViewTestCases:
1227
1226
  For some models this may just be any random objects, but when we have FKs with `on_delete=models.PROTECT`
1228
1227
  (as is often the case) we need to find or create an instance that doesn't have such entanglements.
1229
1228
  """
1230
- return testing.get_deletable_objects(self.model, self._get_queryset()).values_list("pk", flat=True)[:3]
1229
+ return utils.get_deletable_objects(self.model, self._get_queryset()).values_list("pk", flat=True)[:3]
1231
1230
 
1232
1231
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
1233
1232
  def test_bulk_delete_objects_without_permission(self):
@@ -1239,7 +1238,7 @@ class ViewTestCases:
1239
1238
  }
1240
1239
 
1241
1240
  # Try POST without permission
1242
- with testing.disable_warnings("django.request"):
1241
+ with utils.disable_warnings("django.request"):
1243
1242
  self.assertHttpStatus(self.client.post(self._get_url("bulk_delete"), data), 403)
1244
1243
 
1245
1244
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
@@ -1284,7 +1283,7 @@ class ViewTestCases:
1284
1283
  # Try POST with the selected data first. Emulating selecting all -> pressing Delete Selected button.
1285
1284
  response = self.client.post(self._get_url("bulk_delete"), selected_data)
1286
1285
  self.assertHttpStatus(response, 200)
1287
- response_body = testing.extract_page_body(response.content.decode(response.charset))
1286
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
1288
1287
  # Check if all the pks are passed into the BulkDeleteForm/BulkDestroyForm
1289
1288
  for pk in pk_list:
1290
1289
  self.assertIn(f'<input type="hidden" name="pk" value="{pk}"', response_body)
@@ -1312,7 +1311,7 @@ class ViewTestCases:
1312
1311
  response = self.client.post(f"{self._get_url('bulk_delete')}?{query_string}", selected_data)
1313
1312
  # Expect a 200 status cause we are only rendering the bulk delete table after pressing Delete Selected button.
1314
1313
  self.assertHttpStatus(response, 200)
1315
- response_body = testing.extract_page_body(response.content.decode(response.charset))
1314
+ response_body = utils.extract_page_body(response.content.decode(response.charset))
1316
1315
  # Check if the first and second pk is passed into the form.
1317
1316
  self.assertIn(f'<input type="hidden" name="pk" value="{first_pk}"', response_body)
1318
1317
  self.assertIn(f'<input type="hidden" name="pk" value="{second_pk}"', response_body)
@@ -1372,11 +1371,11 @@ class ViewTestCases:
1372
1371
  data.update(self.rename_data)
1373
1372
 
1374
1373
  # Test GET without permission
1375
- with testing.disable_warnings("django.request"):
1374
+ with utils.disable_warnings("django.request"):
1376
1375
  self.assertHttpStatus(self.client.get(self._get_url("bulk_rename")), 403)
1377
1376
 
1378
1377
  # Try POST without permission
1379
- with testing.disable_warnings("django.request"):
1378
+ with utils.disable_warnings("django.request"):
1380
1379
  self.assertHttpStatus(self.client.post(self._get_url("bulk_rename"), data), 403)
1381
1380
 
1382
1381
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -1517,13 +1516,13 @@ class ViewTestCases:
1517
1516
  url = reverse(f"dcim:device_bulk_add_{self.model._meta.model_name}")
1518
1517
  request = {
1519
1518
  "path": url,
1520
- "data": testing.post_data({"pk": data["device"]}),
1519
+ "data": utils.post_data({"pk": data["device"]}),
1521
1520
  }
1522
1521
  else:
1523
1522
  url = reverse(f"virtualization:virtualmachine_bulk_add_{self.model._meta.model_name}")
1524
1523
  request = {
1525
1524
  "path": url,
1526
- "data": testing.post_data({"pk": data["virtual_machine"]}),
1525
+ "data": utils.post_data({"pk": data["virtual_machine"]}),
1527
1526
  }
1528
1527
  self.assertHttpStatus(self.client.post(**request), 200)
1529
1528
 
@@ -1533,7 +1532,7 @@ class ViewTestCases:
1533
1532
  else:
1534
1533
  data["pk"] = data.pop("virtual_machine")
1535
1534
  data["_create"] = ""
1536
- request["data"] = testing.post_data(data)
1535
+ request["data"] = utils.post_data(data)
1537
1536
  self.assertHttpStatus(self.client.post(**request), 302)
1538
1537
 
1539
1538
  updated_count = self._get_queryset().count()
@@ -20,7 +20,7 @@ from nautobot.circuits.models import Provider
20
20
  from nautobot.core import testing
21
21
  from nautobot.core.api.parsers import NautobotCSVParser
22
22
  from nautobot.core.api.renderers import NautobotCSVRenderer
23
- from nautobot.core.api.utils import get_serializer_for_model
23
+ from nautobot.core.api.utils import get_serializer_for_model, get_view_name
24
24
  from nautobot.core.api.versioning import NautobotAPIVersioning
25
25
  from nautobot.core.constants import COMPOSITE_KEY_SEPARATOR
26
26
  from nautobot.core.utils.lookup import get_route_for_model
@@ -28,7 +28,7 @@ from nautobot.dcim import models as dcim_models
28
28
  from nautobot.dcim.api import serializers as dcim_serializers
29
29
  from nautobot.extras import choices, models as extras_models
30
30
  from nautobot.ipam import models as ipam_models
31
- from nautobot.ipam.api import serializers as ipam_serializers
31
+ from nautobot.ipam.api import serializers as ipam_serializers, views as ipam_api_views
32
32
  from nautobot.tenancy import models as tenancy_models
33
33
 
34
34
  User = get_user_model()
@@ -997,3 +997,24 @@ class NewUIGetMenuAPIViewTestCase(testing.APITestCase):
997
997
 
998
998
  self.assertEqual(response.status_code, 200)
999
999
  self.assertEqual(response.data, expected_response)
1000
+
1001
+
1002
+ class NautobotGetViewNameTest(TestCase):
1003
+ """
1004
+ Some unit tests for the get_view_name() functionality.
1005
+ """
1006
+
1007
+ @override_settings(ALLOWED_HOSTS=["*"])
1008
+ def test_get(self):
1009
+ """Assert that the proper view name is displayed for the correct view."""
1010
+ viewset = ipam_api_views.PrefixViewSet
1011
+ # We need to get a specific view, so we need to set the class kwargs
1012
+ view_kwargs = {
1013
+ "Prefixes": {"suffix": "List", "basename": "prefix", "detail": False},
1014
+ "Prefix": {"suffix": "Instance", "basename": "prefix", "detail": True},
1015
+ "Available IPs": {"name": "Available IPs"},
1016
+ "Available Prefixes": {"name": "Available Prefixes"},
1017
+ "Notes": {"name": "Notes"},
1018
+ }
1019
+ for view_name, view_kwarg in view_kwargs.items():
1020
+ self.assertEqual(view_name, get_view_name(viewset(**view_kwarg)))
@@ -4,9 +4,9 @@ from django.contrib.contenttypes.models import ContentType
4
4
  import yaml
5
5
 
6
6
  from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
7
- from nautobot.dcim.models import DeviceType, Manufacturer
7
+ from nautobot.dcim.models import DeviceType, Location, LocationType, Manufacturer
8
8
  from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices
9
- from nautobot.extras.models import ExportTemplate, JobLogEntry, Status
9
+ from nautobot.extras.models import Contact, ContactAssociation, ExportTemplate, JobLogEntry, Role, Status
10
10
  from nautobot.users.models import ObjectPermission
11
11
 
12
12
 
@@ -256,3 +256,80 @@ class ImportObjectsTestCase(TransactionTestCase):
256
256
  self.assertEqual(log_successes[3].message, 'Row 5: Created record "test_status4"')
257
257
  self.assertTrue(Status.objects.filter(name="test_status4").exists())
258
258
  self.assertEqual(log_successes[4].message, "Created 4 status object(s) from 5 row(s) of data")
259
+
260
+ def test_csv_import_contact_assignment(self):
261
+ location_types_csv = "\n".join(["name", "ContactAssignmentImportTestLocationType"])
262
+ locations_csv = "\n".join(
263
+ [
264
+ "location_type__name,name,status__name",
265
+ "ContactAssignmentImportTestLocationType,ContactAssignmentImportTestLocation1,Active",
266
+ "ContactAssignmentImportTestLocationType,ContactAssignmentImportTestLocation2,Active",
267
+ ]
268
+ )
269
+ roles_csv = "\n".join(
270
+ [
271
+ "name,content_types",
272
+ "ContactAssignmentImportTestLocation-On Site,extras.contactassociation",
273
+ ]
274
+ )
275
+ contacts_csv = "\n".join(["name,email", "Bob-ContactAssignmentImportTestLocation,bob@example.com"])
276
+
277
+ location_types_job_result = create_job_result_and_run_job(
278
+ "nautobot.core.jobs",
279
+ "ImportObjects",
280
+ content_type=ContentType.objects.get_for_model(LocationType).pk,
281
+ csv_data=location_types_csv,
282
+ )
283
+ self.assertEqual(location_types_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
284
+
285
+ location_type_count = LocationType.objects.filter(name="ContactAssignmentImportTestLocationType").count()
286
+ self.assertEqual(location_type_count, 1, f"Unexpected count of LocationTypes {location_type_count}")
287
+
288
+ locations_job_result = create_job_result_and_run_job(
289
+ "nautobot.core.jobs",
290
+ "ImportObjects",
291
+ content_type=ContentType.objects.get_for_model(Location).pk,
292
+ csv_data=locations_csv,
293
+ )
294
+ self.assertEqual(locations_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
295
+
296
+ location_count = Location.objects.filter(location_type__name="ContactAssignmentImportTestLocationType").count()
297
+ self.assertEqual(location_count, 2, f"Unexpected count of Locations {location_count}")
298
+
299
+ contacts_job_result = create_job_result_and_run_job(
300
+ "nautobot.core.jobs",
301
+ "ImportObjects",
302
+ content_type=ContentType.objects.get_for_model(Contact).pk,
303
+ csv_data=contacts_csv,
304
+ )
305
+ self.assertEqual(contacts_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
306
+
307
+ contact_count = Contact.objects.filter(name="Bob-ContactAssignmentImportTestLocation").count()
308
+ self.assertEqual(contact_count, 1, f"Unexpected number of contacts {contact_count}")
309
+
310
+ roles_job_result = create_job_result_and_run_job(
311
+ "nautobot.core.jobs",
312
+ "ImportObjects",
313
+ content_type=ContentType.objects.get_for_model(Role).pk,
314
+ csv_data=roles_csv,
315
+ )
316
+ self.assertEqual(roles_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
317
+
318
+ role_count = Role.objects.filter(name="ContactAssignmentImportTestLocation-On Site").count()
319
+ self.assertEqual(role_count, 1, f"Unexpected number of role values {role_count}")
320
+
321
+ associations = ["associated_object_id,associated_object_type,status__name,role__name,contact__name"]
322
+ for location in Location.objects.filter(location_type__name="ContactAssignmentImportTestLocationType"):
323
+ associations.append(
324
+ f"{location.pk},dcim.location,Active,ContactAssignmentImportTestLocation-On Site,Bob-ContactAssignmentImportTestLocation"
325
+ )
326
+ associations_csv = "\n".join(associations)
327
+
328
+ associations_job_result = create_job_result_and_run_job(
329
+ "nautobot.core.jobs",
330
+ "ImportObjects",
331
+ content_type=ContentType.objects.get_for_model(ContactAssociation).pk,
332
+ csv_data=associations_csv,
333
+ )
334
+
335
+ self.assertEqual(associations_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
@@ -283,3 +283,35 @@ class NautobotTemplatetagsHelperTest(TestCase):
283
283
  helpers.support_message(),
284
284
  "<p>Settings <strong>support</strong> message:</p><ul><li>Item 1</li><li>Item 2</li></ul>",
285
285
  )
286
+
287
+ def test_hyperlinked_object_target_new_tab(self):
288
+ # None gives a placeholder
289
+ self.assertEqual(helpers.hyperlinked_object_target_new_tab(None), helpers.placeholder(None))
290
+ # An object without get_absolute_url gives a string
291
+ self.assertEqual(helpers.hyperlinked_object_target_new_tab("hello"), "hello")
292
+ # An object with get_absolute_url gives a hyperlink
293
+ location = models.Location.objects.first()
294
+ # Initially remove description if any
295
+ location.description = ""
296
+ location.save()
297
+ self.assertEqual(
298
+ helpers.hyperlinked_object_target_new_tab(location),
299
+ f'<a href="/dcim/locations/{location.pk}/" target="_blank" rel="noreferrer">{location.name}</a>',
300
+ )
301
+ # An object with get_absolute_url and a description gives a titled hyperlink
302
+ location.description = "An important location"
303
+ location.save()
304
+ self.assertEqual(
305
+ helpers.hyperlinked_object_target_new_tab(location),
306
+ f'<a href="/dcim/locations/{location.pk}/" title="An important location" target="_blank" rel="noreferrer">{location.name}</a>',
307
+ )
308
+ # Optionally you can request a field other than the object's display string
309
+ self.assertEqual(
310
+ helpers.hyperlinked_object_target_new_tab(location, "name"),
311
+ f'<a href="/dcim/locations/{location.pk}/" title="An important location" target="_blank" rel="noreferrer">{location.name}</a>',
312
+ )
313
+ # If you request a nonexistent field, it defaults to the string representation
314
+ self.assertEqual(
315
+ helpers.hyperlinked_object_target_new_tab(location, "foo"),
316
+ f'<a href="/dcim/locations/{location.pk}/" title="An important location" target="_blank" rel="noreferrer">{location!s}</a>',
317
+ )
@@ -146,6 +146,39 @@ class HomeViewTestCase(TestCase):
146
146
  response_content = response.content.decode(response.charset).replace("\n", "")
147
147
  self.assertNotRegex(response_content, footer_hostname_version_pattern)
148
148
 
149
+ def test_banners_markdown(self):
150
+ url = reverse("home")
151
+ with override_settings(
152
+ BANNER_TOP="# Hello world",
153
+ BANNER_BOTTOM="[info](https://nautobot.com)",
154
+ ):
155
+ response = self.client.get(url)
156
+ self.assertInHTML("<h1>Hello world</h1>", response.content.decode(response.charset))
157
+ self.assertInHTML(
158
+ '<a href="https://nautobot.com" rel="noopener noreferrer">info</a>',
159
+ response.content.decode(response.charset),
160
+ )
161
+
162
+ with override_settings(BANNER_LOGIN="_Welcome to Nautobot!_"):
163
+ self.client.logout()
164
+ response = self.client.get(reverse("login"))
165
+ self.assertInHTML("<em>Welcome to Nautobot!</em>", response.content.decode(response.charset))
166
+
167
+ def test_banners_no_xss(self):
168
+ url = reverse("home")
169
+ with override_settings(
170
+ BANNER_TOP='<script>alert("Hello from above!");</script>',
171
+ BANNER_BOTTOM='<script>alert("Hello from below!");</script>',
172
+ ):
173
+ response = self.client.get(url)
174
+ self.assertNotIn("Hello from above", response.content.decode(response.charset))
175
+ self.assertNotIn("Hello from below", response.content.decode(response.charset))
176
+
177
+ with override_settings(BANNER_LOGIN='<script>alert("Welcome to Nautobot!");</script>'):
178
+ self.client.logout()
179
+ response = self.client.get(reverse("login"))
180
+ self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
181
+
149
182
 
150
183
  @override_settings(BRANDING_TITLE="Nautobot")
151
184
  class SearchFieldsTestCase(TestCase):
@@ -242,6 +275,25 @@ class FilterFormsTestCase(TestCase):
242
275
  self.assertInHTML(locations[0].name, response_content)
243
276
  self.assertInHTML(locations[1].name, response_content)
244
277
 
278
+ def test_filtering_crafted_query_params(self):
279
+ """Test for reflected-XSS vulnerability GHSA-jxgr-gcj5-cqqg."""
280
+ self.add_permissions("dcim.view_location")
281
+ query_param = "?location_type=1 onmouseover=alert('hi') foo=bar"
282
+ url = reverse("dcim:location_list") + query_param
283
+ response = self.client.get(url)
284
+ self.assertHttpStatus(response, 200)
285
+ response_content = response.content.decode(response.charset)
286
+ # The important thing here is that the data-field-parent and data-field-value are correctly quoted
287
+ self.assertInHTML(
288
+ """
289
+ <span class="filter-selection-choice-remove remove-filter-param"
290
+ data-field-type="child"
291
+ data-field-parent="location_type"
292
+ data-field-value="1 onmouseover=alert(&#x27;hi&#x27;) foo=bar"
293
+ >×</span>""", # noqa: RUF001 - ambiguous-unicode-character-string
294
+ response_content,
295
+ )
296
+
245
297
 
246
298
  class ForceScriptNameTestcase(TestCase):
247
299
  """Basic test to assert that `settings.FORCE_SCRIPT_NAME` works as intended."""