nautobot 2.4.8__py3-none-any.whl → 2.4.10__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 (387) hide show
  1. nautobot/circuits/templates/circuits/circuittype.html +1 -1
  2. nautobot/circuits/templates/circuits/circuittype_retrieve.html +1 -39
  3. nautobot/circuits/views.py +18 -23
  4. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +4 -111
  5. nautobot/cloud/views.py +56 -25
  6. nautobot/core/api/parsers.py +56 -2
  7. nautobot/core/celery/schedulers.py +1 -0
  8. nautobot/core/filters.py +1 -1
  9. nautobot/core/graphql/schema.py +1 -0
  10. nautobot/core/jobs/__init__.py +14 -3
  11. nautobot/core/models/__init__.py +2 -0
  12. nautobot/core/tables.py +13 -6
  13. nautobot/core/testing/views.py +27 -2
  14. nautobot/core/tests/test_csv.py +92 -1
  15. nautobot/core/tests/test_jinja_filters.py +59 -0
  16. nautobot/core/tests/test_jobs.py +113 -0
  17. nautobot/core/tests/test_ui.py +53 -1
  18. nautobot/core/tests/test_utils.py +11 -0
  19. nautobot/core/tests/test_views.py +73 -0
  20. nautobot/core/ui/object_detail.py +19 -12
  21. nautobot/core/urls.py +2 -2
  22. nautobot/core/utils/filtering.py +3 -0
  23. nautobot/core/views/__init__.py +21 -0
  24. nautobot/core/views/renderers.py +1 -1
  25. nautobot/dcim/forms.py +10 -0
  26. nautobot/dcim/models/device_component_templates.py +4 -0
  27. nautobot/dcim/models/device_components.py +12 -0
  28. nautobot/dcim/models/devices.py +6 -0
  29. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -43
  30. nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +1 -59
  31. nautobot/dcim/templates/dcim/devicetype.html +2 -217
  32. nautobot/dcim/templates/dcim/devicetype_edit.html +2 -32
  33. nautobot/dcim/templates/dcim/devicetype_retrieve.html +217 -0
  34. nautobot/dcim/templates/dcim/devicetype_update.html +32 -0
  35. nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
  36. nautobot/dcim/templates/dcim/modulebay_retrieve.html +1 -84
  37. nautobot/dcim/templates/dcim/rack_elevation.html +14 -0
  38. nautobot/dcim/templates/dcim/rackreservation_retrieve.html +0 -68
  39. nautobot/dcim/tests/integration/test_fileinputpicker.py +1 -1
  40. nautobot/dcim/urls.py +1 -36
  41. nautobot/dcim/views.py +133 -81
  42. nautobot/extras/api/views.py +4 -6
  43. nautobot/extras/context_managers.py +2 -2
  44. nautobot/extras/migrations/0024_job_data_migration.py +1 -1
  45. nautobot/extras/models/customfields.py +2 -0
  46. nautobot/extras/models/datasources.py +8 -0
  47. nautobot/extras/models/groups.py +18 -0
  48. nautobot/extras/models/jobs.py +92 -62
  49. nautobot/extras/models/metadata.py +2 -0
  50. nautobot/extras/models/models.py +4 -0
  51. nautobot/extras/models/secrets.py +7 -0
  52. nautobot/extras/secrets/__init__.py +14 -0
  53. nautobot/extras/tables.py +11 -1
  54. nautobot/extras/templates/extras/computedfield_retrieve.html +1 -55
  55. nautobot/extras/templates/extras/inc/job_tiles.html +1 -1
  56. nautobot/extras/templates/extras/jobresult.html +1 -1
  57. nautobot/extras/templates/extras/metadatatype_retrieve.html +1 -66
  58. nautobot/extras/templates/extras/scheduledjob.html +25 -9
  59. nautobot/extras/tests/test_api.py +1 -1
  60. nautobot/extras/tests/test_context_managers.py +20 -0
  61. nautobot/extras/tests/test_models.py +26 -0
  62. nautobot/extras/tests/test_views.py +15 -2
  63. nautobot/extras/utils.py +18 -16
  64. nautobot/extras/views.py +65 -26
  65. nautobot/ipam/models.py +32 -0
  66. nautobot/ipam/tables.py +3 -4
  67. nautobot/project-static/docs/404.html +4 -4
  68. nautobot/project-static/docs/apps/index.html +4 -4
  69. nautobot/project-static/docs/apps/nautobot-apps.html +4 -4
  70. nautobot/project-static/docs/assets/javascripts/{bundle.c8b220af.min.js → bundle.13a4f30d.min.js} +4 -4
  71. nautobot/project-static/docs/assets/javascripts/{bundle.c8b220af.min.js.map → bundle.13a4f30d.min.js.map} +2 -2
  72. nautobot/project-static/docs/assets/javascripts/workers/{search.f8cc74c7.min.js → search.d50fe291.min.js} +2 -2
  73. nautobot/project-static/docs/assets/javascripts/workers/{search.f8cc74c7.min.js.map → search.d50fe291.min.js.map} +1 -1
  74. nautobot/project-static/docs/assets/stylesheets/{main.2afb09e1.min.css → main.342714a4.min.css} +1 -1
  75. nautobot/project-static/docs/assets/stylesheets/{main.2afb09e1.min.css.map → main.342714a4.min.css.map} +1 -1
  76. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +4 -4
  77. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +4 -4
  78. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +4 -4
  79. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +4 -4
  80. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +4 -4
  81. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +4 -4
  82. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +4 -4
  83. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +4 -4
  84. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +4 -4
  85. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +4 -4
  86. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +4 -4
  87. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +4 -4
  88. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
  89. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +4 -4
  90. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +4 -4
  91. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +4 -4
  92. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +4 -4
  93. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +4 -4
  94. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +4 -4
  95. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +4 -4
  96. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +6 -8
  97. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +4 -4
  98. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +36 -6
  99. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +4 -4
  100. nautobot/project-static/docs/development/apps/api/configuration-view.html +4 -4
  101. nautobot/project-static/docs/development/apps/api/database-backend-config.html +4 -4
  102. nautobot/project-static/docs/development/apps/api/models/django-admin.html +4 -4
  103. nautobot/project-static/docs/development/apps/api/models/global-search.html +4 -4
  104. nautobot/project-static/docs/development/apps/api/models/graphql.html +4 -4
  105. nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
  106. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +4 -4
  107. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +4 -4
  108. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +4 -4
  109. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +4 -4
  110. nautobot/project-static/docs/development/apps/api/platform-features/index.html +4 -4
  111. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +4 -4
  112. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +4 -4
  113. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +4 -4
  114. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +43 -42
  115. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +4 -4
  116. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +4 -4
  117. nautobot/project-static/docs/development/apps/api/prometheus.html +4 -4
  118. nautobot/project-static/docs/development/apps/api/setup.html +4 -4
  119. nautobot/project-static/docs/development/apps/api/testing.html +4 -4
  120. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +4 -4
  121. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +4 -4
  122. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +4 -4
  123. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +4 -4
  124. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +4 -4
  125. nautobot/project-static/docs/development/apps/api/views/base-template.html +4 -4
  126. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +4 -4
  127. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +4 -4
  128. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +4 -4
  129. nautobot/project-static/docs/development/apps/api/views/index.html +4 -4
  130. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +4 -4
  131. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +4 -4
  132. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +4 -4
  133. nautobot/project-static/docs/development/apps/api/views/notes.html +4 -4
  134. nautobot/project-static/docs/development/apps/api/views/rest-api.html +4 -4
  135. nautobot/project-static/docs/development/apps/api/views/urls.html +4 -4
  136. nautobot/project-static/docs/development/apps/index.html +4 -4
  137. nautobot/project-static/docs/development/apps/migration/code-updates.html +4 -4
  138. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +4 -4
  139. nautobot/project-static/docs/development/apps/migration/from-v1.html +4 -4
  140. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +4 -4
  141. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +4 -4
  142. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +4 -4
  143. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +4 -4
  144. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +4 -4
  145. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +4 -4
  146. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +4 -4
  147. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +4 -4
  148. nautobot/project-static/docs/development/apps/porting-from-netbox.html +4 -4
  149. nautobot/project-static/docs/development/core/application-registry.html +4 -4
  150. nautobot/project-static/docs/development/core/best-practices.html +4 -4
  151. nautobot/project-static/docs/development/core/bootstrap-ui.html +4 -4
  152. nautobot/project-static/docs/development/core/caching.html +4 -4
  153. nautobot/project-static/docs/development/core/controllers.html +4 -4
  154. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +41 -55
  155. nautobot/project-static/docs/development/core/generic-views.html +4 -4
  156. nautobot/project-static/docs/development/core/getting-started.html +4 -4
  157. nautobot/project-static/docs/development/core/homepage.html +4 -4
  158. nautobot/project-static/docs/development/core/index.html +4 -4
  159. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +4 -4
  160. nautobot/project-static/docs/development/core/model-checklist.html +4 -4
  161. nautobot/project-static/docs/development/core/model-features.html +4 -4
  162. nautobot/project-static/docs/development/core/natural-keys.html +4 -4
  163. nautobot/project-static/docs/development/core/navigation-menu.html +4 -4
  164. nautobot/project-static/docs/development/core/release-checklist.html +4 -4
  165. nautobot/project-static/docs/development/core/role-internals.html +4 -4
  166. nautobot/project-static/docs/development/core/settings.html +4 -4
  167. nautobot/project-static/docs/development/core/style-guide.html +4 -4
  168. nautobot/project-static/docs/development/core/templates.html +4 -4
  169. nautobot/project-static/docs/development/core/testing.html +4 -4
  170. nautobot/project-static/docs/development/core/ui-component-framework.html +4 -4
  171. nautobot/project-static/docs/development/core/user-preferences.html +4 -4
  172. nautobot/project-static/docs/development/index.html +4 -4
  173. nautobot/project-static/docs/development/jobs/getting-started.html +4 -4
  174. nautobot/project-static/docs/development/jobs/index.html +4 -4
  175. nautobot/project-static/docs/development/jobs/installation.html +4 -4
  176. nautobot/project-static/docs/development/jobs/job-extensions.html +4 -4
  177. nautobot/project-static/docs/development/jobs/job-logging.html +4 -4
  178. nautobot/project-static/docs/development/jobs/job-patterns.html +4 -4
  179. nautobot/project-static/docs/development/jobs/job-structure.html +4 -4
  180. nautobot/project-static/docs/development/jobs/migration/from-v1.html +4 -4
  181. nautobot/project-static/docs/development/jobs/testing.html +4 -4
  182. nautobot/project-static/docs/index.html +4 -4
  183. nautobot/project-static/docs/overview/application_stack.html +4 -4
  184. nautobot/project-static/docs/overview/design_philosophy.html +4 -4
  185. nautobot/project-static/docs/release-notes/index.html +4 -4
  186. nautobot/project-static/docs/release-notes/version-1.0.html +4 -4
  187. nautobot/project-static/docs/release-notes/version-1.1.html +4 -4
  188. nautobot/project-static/docs/release-notes/version-1.2.html +4 -4
  189. nautobot/project-static/docs/release-notes/version-1.3.html +4 -4
  190. nautobot/project-static/docs/release-notes/version-1.4.html +4 -4
  191. nautobot/project-static/docs/release-notes/version-1.5.html +4 -4
  192. nautobot/project-static/docs/release-notes/version-1.6.html +301 -4
  193. nautobot/project-static/docs/release-notes/version-2.0.html +4 -4
  194. nautobot/project-static/docs/release-notes/version-2.1.html +4 -4
  195. nautobot/project-static/docs/release-notes/version-2.2.html +4 -4
  196. nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
  197. nautobot/project-static/docs/release-notes/version-2.4.html +291 -4
  198. nautobot/project-static/docs/requirements.txt +1 -1
  199. nautobot/project-static/docs/search/search_index.json +1 -1
  200. nautobot/project-static/docs/sitemap.xml +298 -298
  201. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  202. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +4 -4
  203. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +4 -4
  204. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
  205. nautobot/project-static/docs/user-guide/administration/configuration/index.html +4 -4
  206. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +4 -4
  207. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +4 -4
  208. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +4 -4
  209. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +4 -4
  210. nautobot/project-static/docs/user-guide/administration/guides/docker.html +4 -4
  211. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +4 -4
  212. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +4 -4
  213. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
  214. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +4 -4
  215. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +4 -4
  216. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +4 -4
  217. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +4 -4
  218. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +4 -4
  219. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +4 -4
  220. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +4 -4
  221. nautobot/project-static/docs/user-guide/administration/installation/index.html +4 -4
  222. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +4 -4
  223. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +4 -4
  224. nautobot/project-static/docs/user-guide/administration/installation/services.html +4 -4
  225. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +4 -4
  226. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +4 -4
  227. nautobot/project-static/docs/user-guide/administration/security/index.html +4 -5
  228. nautobot/project-static/docs/user-guide/administration/security/notices.html +117 -5
  229. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +4 -4
  230. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +4 -4
  231. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +4 -4
  232. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +4 -4
  233. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +4 -4
  234. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +4 -4
  235. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +4 -4
  236. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +4 -4
  237. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +4 -4
  238. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +4 -4
  239. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +4 -4
  240. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +4 -4
  241. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +4 -4
  242. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +4 -4
  243. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +4 -4
  244. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +4 -4
  245. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +4 -4
  246. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +4 -4
  247. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +4 -4
  248. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +4 -4
  249. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +4 -4
  250. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +4 -4
  251. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +4 -4
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +4 -4
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +4 -4
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +4 -4
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +4 -4
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +4 -4
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +4 -4
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +4 -4
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +4 -4
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +4 -4
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +4 -4
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +4 -4
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +4 -4
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +4 -4
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +4 -4
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +4 -4
  267. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +4 -4
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +4 -4
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +4 -4
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +4 -4
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +4 -4
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +4 -4
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +4 -4
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +4 -4
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +4 -4
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +4 -4
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +4 -4
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +4 -4
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +4 -4
  280. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +4 -4
  281. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +4 -4
  282. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +4 -4
  283. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +4 -4
  284. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +4 -4
  285. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +4 -4
  286. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +4 -4
  287. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +4 -4
  288. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +4 -4
  289. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +4 -4
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -4
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +4 -4
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +4 -4
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +4 -4
  294. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +4 -4
  295. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +4 -4
  296. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +4 -4
  297. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +4 -4
  298. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +4 -4
  299. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +4 -4
  300. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +4 -4
  301. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +4 -4
  302. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +4 -4
  303. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +4 -4
  304. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +4 -4
  305. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +4 -4
  306. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +4 -4
  307. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +4 -4
  308. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +4 -4
  309. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +4 -4
  310. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +4 -4
  311. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +4 -4
  312. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +4 -4
  313. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +4 -4
  314. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +4 -4
  315. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +4 -4
  316. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +4 -4
  317. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +4 -4
  318. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +4 -4
  319. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +4 -4
  320. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +4 -4
  321. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +4 -4
  322. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +4 -4
  323. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +4 -4
  324. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +4 -4
  325. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
  326. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +4 -4
  327. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +4 -4
  328. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +4 -4
  329. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +4 -4
  330. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +4 -4
  331. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +4 -4
  332. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +4 -4
  333. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +4 -4
  334. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +4 -4
  335. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +4 -4
  336. nautobot/project-static/docs/user-guide/index.html +4 -4
  337. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +4 -4
  338. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +4 -4
  339. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +4 -4
  340. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +4 -4
  341. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +4 -4
  342. nautobot/project-static/docs/user-guide/platform-functionality/events.html +4 -4
  343. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +4 -4
  344. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +4 -4
  345. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +4 -4
  346. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +4 -4
  347. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +4 -4
  348. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +4 -4
  349. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +4 -4
  350. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +4 -4
  351. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +4 -4
  352. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +4 -4
  353. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +4 -4
  354. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +4 -4
  355. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +4 -4
  356. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
  357. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +4 -4
  358. nautobot/project-static/docs/user-guide/platform-functionality/note.html +4 -4
  359. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +4 -4
  360. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +4 -4
  361. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +4 -4
  362. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +4 -4
  363. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +4 -4
  364. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +4 -4
  365. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +4 -4
  366. nautobot/project-static/docs/user-guide/platform-functionality/role.html +4 -4
  367. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +4 -4
  368. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +4 -4
  369. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +4 -4
  370. nautobot/project-static/docs/user-guide/platform-functionality/status.html +4 -4
  371. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +4 -4
  372. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +4 -4
  373. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +4 -4
  374. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +4 -4
  375. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +4 -4
  376. nautobot/project-static/js/forms.js +0 -14
  377. nautobot/users/models.py +4 -0
  378. nautobot/virtualization/models.py +4 -0
  379. nautobot/wireless/tables.py +1 -0
  380. nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +1 -55
  381. nautobot/wireless/views.py +33 -28
  382. {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/METADATA +4 -4
  383. {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/RECORD +387 -384
  384. {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/LICENSE.txt +0 -0
  385. {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/NOTICE +0 -0
  386. {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/WHEEL +0 -0
  387. {nautobot-2.4.8.dist-info → nautobot-2.4.10.dist-info}/entry_points.txt +0 -0
@@ -7,7 +7,7 @@ from django.urls import reverse
7
7
 
8
8
  from nautobot.core.constants import CSV_NO_OBJECT, CSV_NULL_TYPE, VARBINARY_IP_FIELD_REPR_OF_CSV_NO_OBJECT
9
9
  from nautobot.dcim.api.serializers import DeviceSerializer
10
- from nautobot.dcim.models.devices import Controller, Device, DeviceType
10
+ from nautobot.dcim.models.devices import Controller, Device, DeviceType, Platform, SoftwareImageFile, SoftwareVersion
11
11
  from nautobot.dcim.models.locations import Location
12
12
  from nautobot.extras.models.roles import Role
13
13
  from nautobot.extras.models.statuses import Status
@@ -317,3 +317,94 @@ class CSVParsingRelatedTestCase(TestCase):
317
317
  tenant=self.device2.tenant,
318
318
  )
319
319
  self.assertEqual(device4.tags.count(), 0)
320
+
321
+ @override_settings(ALLOWED_HOSTS=["*"])
322
+ def test_m2m_field_import(self):
323
+ """Test CSV import of M2M field."""
324
+
325
+ platform = Platform.objects.first()
326
+ software_version_status = Status.objects.get_for_model(SoftwareVersion).first()
327
+ software_image_file_status = Status.objects.get_for_model(SoftwareImageFile).first()
328
+
329
+ software_version = SoftwareVersion.objects.create(
330
+ platform=platform, version="Test version 1.0.0", status=software_version_status
331
+ )
332
+ software_image_files = (
333
+ SoftwareImageFile.objects.create(
334
+ software_version=software_version,
335
+ image_file_name="software_image_file_qs_test_1.bin",
336
+ status=software_image_file_status,
337
+ ),
338
+ SoftwareImageFile.objects.create(
339
+ software_version=software_version,
340
+ image_file_name="software_image_file_qs_test_2.bin",
341
+ status=software_image_file_status,
342
+ default_image=True,
343
+ ),
344
+ SoftwareImageFile.objects.create(
345
+ software_version=software_version,
346
+ image_file_name="software_image_file_qs_test_3.bin",
347
+ status=software_image_file_status,
348
+ ),
349
+ )
350
+
351
+ user = UserFactory.create()
352
+ user.is_superuser = True
353
+ user.is_active = True
354
+ user.save()
355
+ self.client.force_login(user)
356
+
357
+ with self.subTest("Import M2M field using list of UUIDs"):
358
+ import_data = f"""name,device_type,location,role,status,software_image_files
359
+ TestDevice5,{self.device.device_type.pk},{self.device.location.pk},{self.device.role.pk},{self.device.status.pk},"{software_image_files[0].pk},{software_image_files[1].pk}"
360
+ """
361
+ data = {"csv_data": import_data}
362
+ url = reverse("dcim:device_import")
363
+ response = self.client.post(url, data)
364
+
365
+ self.assertEqual(response.status_code, 200)
366
+ self.assertEqual(Device.objects.count(), 3)
367
+
368
+ # Assert TestDevice5 got created with the right fields
369
+ device5 = Device.objects.get(
370
+ name="TestDevice5",
371
+ location=self.device.location,
372
+ device_type=self.device.device_type,
373
+ role=self.device.role,
374
+ status=self.device.status,
375
+ tenant=None,
376
+ )
377
+ self.assertEqual(device5.software_image_files.count(), 2)
378
+
379
+ with self.subTest("Import M2M field using multiple identifying fields"):
380
+ import_data = f"""name,device_type,location,role,status,software_image_files__software_version,software_image_files__image_file_name
381
+ TestDevice6,{self.device.device_type.pk},{self.device.location.pk},{self.device.role.pk},{self.device.status.pk},"{software_version.pk},{software_version.pk}","{software_image_files[0].image_file_name},{software_image_files[1].image_file_name}"
382
+ """
383
+ data = {"csv_data": import_data}
384
+ url = reverse("dcim:device_import")
385
+ response = self.client.post(url, data)
386
+
387
+ self.assertEqual(response.status_code, 200)
388
+ self.assertEqual(Device.objects.count(), 4)
389
+
390
+ # Assert TestDevice5 got created with the right fields
391
+ device6 = Device.objects.get(
392
+ name="TestDevice6",
393
+ location=self.device.location,
394
+ device_type=self.device.device_type,
395
+ role=self.device.role,
396
+ status=self.device.status,
397
+ tenant=None,
398
+ )
399
+ self.assertEqual(device6.software_image_files.count(), 2)
400
+
401
+ with self.subTest("Import M2M field using incorrect number of values"):
402
+ import_data = f"""name,device_type,location,role,status,software_image_files__software_version,software_image_files__image_file_name
403
+ TestDevice7,{self.device.device_type.pk},{self.device.location.pk},{self.device.role.pk},{self.device.status.pk},"{software_version.pk},{software_version.pk}","{software_image_files[0].image_file_name},{software_image_files[1].image_file_name},{software_image_files[2].image_file_name}"
404
+ """
405
+ data = {"csv_data": import_data}
406
+ url = reverse("dcim:device_import")
407
+ response = self.client.post(url, data)
408
+ self.assertEqual(response.status_code, 200)
409
+ self.assertContains(response, "Incorrect number of values provided for the software_image_files field")
410
+ self.assertEqual(Device.objects.count(), 4)
@@ -4,6 +4,8 @@ from netutils.utils import jinja2_convenience_function
4
4
 
5
5
  from nautobot.core.utils import data
6
6
  from nautobot.dcim import models as dcim_models
7
+ from nautobot.extras import models as extras_models
8
+ from nautobot.ipam import models as ipam_models
7
9
 
8
10
 
9
11
  class NautobotJinjaFilterTest(TestCase):
@@ -85,3 +87,60 @@ class NautobotJinjaFilterTest(TestCase):
85
87
  self.fail("SecurityError raised on safe Jinja template render")
86
88
  else:
87
89
  self.assertEqual(value, location.parent.name)
90
+
91
+ def test_render_blocks_various_unsafe_methods(self):
92
+ """Assert that Jinja template rendering correctly blocks various unsafe Nautobot APIs."""
93
+ device = dcim_models.Device.objects.first()
94
+ dynamic_group = extras_models.DynamicGroup.objects.first()
95
+ git_repository = extras_models.GitRepository.objects.create(
96
+ name="repo", slug="repo", remote_url="file:///", branch="main"
97
+ )
98
+ interface = dcim_models.Interface.objects.first()
99
+ interface_template = dcim_models.InterfaceTemplate.objects.first()
100
+ location = dcim_models.Location.objects.first()
101
+ module = dcim_models.Module.objects.first()
102
+ prefix = ipam_models.Prefix.objects.first()
103
+ secret = extras_models.Secret.objects.create(name="secret", provider="environment-variable")
104
+ vrf = ipam_models.VRF.objects.first()
105
+
106
+ context = {
107
+ "device": device,
108
+ "dynamic_group": dynamic_group,
109
+ "git_repository": git_repository,
110
+ "interface": interface,
111
+ "interface_template": interface_template,
112
+ "location": location,
113
+ "module": module,
114
+ "prefix": prefix,
115
+ "secret": secret,
116
+ "vrf": vrf,
117
+ "JobResult": extras_models.JobResult,
118
+ "ScheduledJob": extras_models.ScheduledJob,
119
+ }
120
+
121
+ for call in [
122
+ "device.create_components()",
123
+ "dynamic_group.add_members([])",
124
+ "dynamic_group.remove_members([])",
125
+ "git_repository.sync(None)",
126
+ "git_repository.clone_to_directory()",
127
+ "git_repository.cleanup_cloned_directory('/tmp/')",
128
+ "interface.render_name_template()",
129
+ "interface.add_ip_addresses([])",
130
+ "interface_template.instantiate(device)",
131
+ "interface_template.instantiate_model(interface_template, device)",
132
+ "location.validated_save()",
133
+ "module.create_components()",
134
+ "module.render_component_names()",
135
+ "prefix.reparent_ips()",
136
+ "prefix.reparent_subnets()",
137
+ "secret.get_value()",
138
+ "vrf.add_device(device)",
139
+ "vrf.add_prefix(prefix)",
140
+ "JobResult.enqueue_job(None, None)",
141
+ "JobResult.log('hello world')",
142
+ "ScheduledJob.create_schedule(None, None)",
143
+ ]:
144
+ with self.subTest(call=call):
145
+ with self.assertRaises(SecurityError):
146
+ data.render_jinja2(template_code="{{ " + call + " }}", context=context)
@@ -1,5 +1,7 @@
1
1
  import codecs
2
+ import csv
2
3
  from datetime import timedelta
4
+ from io import StringIO
3
5
  import json
4
6
  from pathlib import Path
5
7
 
@@ -10,6 +12,7 @@ from django.utils import timezone
10
12
  import yaml
11
13
 
12
14
  from nautobot.circuits.models import Circuit, CircuitType, Provider
15
+ from nautobot.core.jobs import ExportObjectList
13
16
  from nautobot.core.jobs.cleanup import CleanupTypes
14
17
  from nautobot.core.testing import create_job_result_and_run_job, TransactionTestCase
15
18
  from nautobot.core.testing.context import load_event_broker_override_settings
@@ -25,6 +28,7 @@ from nautobot.extras.models import (
25
28
  JobResult,
26
29
  ObjectChange,
27
30
  Role,
31
+ SavedView,
28
32
  Status,
29
33
  Tag,
30
34
  )
@@ -40,6 +44,32 @@ class ExportObjectListTest(TransactionTestCase):
40
44
 
41
45
  databases = ("default", "job_logs")
42
46
 
47
+ def _create_saved_view(self, model_class=Status, config=None):
48
+ """Helper to create a SavedView with optional filter config."""
49
+ return SavedView.objects.create(
50
+ name="Global default View",
51
+ owner=self.user,
52
+ view=f"{model_class._meta.app_label}:{model_class._meta.model_name}_list",
53
+ is_global_default=True,
54
+ config=config or {},
55
+ )
56
+
57
+ def _run_export_job(self, query_string, model_class=Status):
58
+ """Helper to run export job and return parsed CSV rows."""
59
+ job_result = create_job_result_and_run_job(
60
+ "nautobot.core.jobs",
61
+ "ExportObjectList",
62
+ content_type=ContentType.objects.get_for_model(model_class).pk,
63
+ query_string=query_string,
64
+ )
65
+ self.assertJobResultStatus(job_result)
66
+ self.assertTrue(job_result.files.exists())
67
+ self.assertEqual(
68
+ Path(job_result.files.first().file.name).name, f"nautobot_{model_class._meta.verbose_name_plural}.csv"
69
+ )
70
+ csv_data = job_result.files.first().file.read().decode("utf-8").lstrip("\ufeff")
71
+ return list(csv.DictReader(StringIO(csv_data)))
72
+
43
73
  def test_export_without_permission(self):
44
74
  """Job should enforce user permissions on the content-type being asked for export."""
45
75
  job_result = create_job_result_and_run_job(
@@ -136,6 +166,89 @@ class ExportObjectListTest(TransactionTestCase):
136
166
  data = yaml.safe_load(yaml_data)
137
167
  self.assertEqual(data["manufacturer"], "Cisco")
138
168
 
169
+ def test_get_saved_view_filter_params(self):
170
+ """Test various cases for the saved view filter parameters."""
171
+ saved_view = self._create_saved_view(config={"filter_params": {"name": ["Active"]}})
172
+ test_cases = [
173
+ # (query_params, expected_output)
174
+ ({"saved_view": saved_view.pk}, {"name": ["Active"]}),
175
+ (
176
+ {
177
+ "saved_view": saved_view.pk,
178
+ "name": ["Active"],
179
+ "content_types": ["dcim.devices"],
180
+ }, # new filter content_types
181
+ {"name": ["Active"]},
182
+ ),
183
+ (
184
+ {"saved_view": saved_view.pk, "content_types": ["dcim.devices"]}, # name filter was deleted
185
+ {},
186
+ ),
187
+ ({"saved_view": saved_view.pk, "all_filters_removed": "true"}, {}),
188
+ (
189
+ {"name": ["Active"]}, # No saved view provided
190
+ {},
191
+ ),
192
+ ]
193
+
194
+ for query_params, expected_output in test_cases:
195
+ with self.subTest(query_params=query_params, expected_output=expected_output):
196
+ job = ExportObjectList()
197
+ filter_params = job._get_saved_view_filter_params(query_params)
198
+ self.assertEqual(filter_params, expected_output)
199
+
200
+ def test_export_saved_view_to_csv_without_filters(self):
201
+ """Export a SavedView to CSV without any filters applied."""
202
+ # URL: /?saved_view=<id>
203
+ sv = self._create_saved_view()
204
+ rows = self._run_export_job(query_string=f"saved_view={sv.pk}")
205
+ self.assertEqual(len(rows), Status.objects.count())
206
+
207
+ def test_export_saved_view_to_csv_with_filters_from_saved_view(self):
208
+ """Export a SavedView to CSV using filters defined in the SavedView config."""
209
+ # URL: /?saved_view=<id>
210
+ filter_name = Status.objects.first().name
211
+ sv = self._create_saved_view(config={"filter_params": {"name": [filter_name]}})
212
+ rows = self._run_export_job(query_string=f"saved_view={sv.pk}")
213
+ self.assertGreaterEqual(Status.objects.count(), 1) # Ensure multiple Statuses exist and filter works
214
+ self.assertEqual(len(rows), 1)
215
+ self.assertEqual(rows[0]["name"], filter_name)
216
+
217
+ def test_export_saved_view_to_csv_with_combined_filters(self):
218
+ """Export a SavedView to CSV using combined filters from SavedView config and query params."""
219
+ # URL: /?saved_view=<id>&name=<filter_name>&name=<filter_name2>
220
+ filter_name = Status.objects.first().name
221
+ filter_name2 = Status.objects.last().name
222
+ sv = self._create_saved_view(config={"filter_params": {"name": [filter_name]}})
223
+ rows = self._run_export_job(query_string=f"saved_view={sv.pk}&name={filter_name}&name={filter_name2}")
224
+ self.assertEqual(len(rows), 2)
225
+ self.assertEqual(rows[0]["name"], filter_name)
226
+ self.assertEqual(rows[1]["name"], filter_name2)
227
+
228
+ def test_export_saved_view_manufacturer_to_csv_with_replaced_filters(self):
229
+ """Export a SavedView manufacturer to CSV after replacing filters."""
230
+ # URL: /?saved_view=<id>&description=<manufacturer2>
231
+ manufacturer = Manufacturer.objects.create(name="Test Manufacturer")
232
+ manufacturer2 = Manufacturer.objects.create(name="Test2 Manufacturer", description="test filter")
233
+ filter_name = manufacturer.name
234
+ filter_description = manufacturer2.description
235
+ sv = self._create_saved_view(model_class=Manufacturer, config={"filter_params": {"name": [filter_name]}})
236
+ rows = self._run_export_job(
237
+ query_string=f"saved_view={sv.pk}&description={filter_description}", model_class=Manufacturer
238
+ )
239
+ self.assertEqual(len(rows), 1)
240
+ self.assertEqual(rows[0]["name"], manufacturer2.name)
241
+ self.assertEqual(rows[0]["description"], filter_description)
242
+ self.assertTrue(all(row["name"] != filter_name for row in rows))
243
+
244
+ def test_export_saved_view_to_csv_after_removing_all_filters(self):
245
+ """Export a SavedView to CSV after removing all filters."""
246
+ # URL: /?saved_view=<id>&all_filters_removed=true
247
+ filter_name = Status.objects.first().name
248
+ sv = self._create_saved_view(config={"filter_params": {"name": [filter_name]}})
249
+ rows = self._run_export_job(query_string=f"saved_view={sv.pk}&all_filters_removed=true")
250
+ self.assertEqual(len(rows), Status.objects.count())
251
+
139
252
 
140
253
  class ImportObjectsTestCase(TransactionTestCase):
141
254
  databases = ("default", "job_logs")
@@ -3,10 +3,13 @@
3
3
  from unittest.mock import patch
4
4
 
5
5
  from django.template import Context
6
+ from django.test import RequestFactory
6
7
 
7
8
  from nautobot.core.templatetags.helpers import HTML_NONE
8
9
  from nautobot.core.testing import TestCase
9
- from nautobot.core.ui.object_detail import BaseTextPanel, DataTablePanel, Panel
10
+ from nautobot.core.ui.object_detail import BaseTextPanel, DataTablePanel, ObjectsTablePanel, Panel
11
+ from nautobot.dcim.models import DeviceRedundancyGroup
12
+ from nautobot.dcim.tables.devices import DeviceTable
10
13
 
11
14
 
12
15
  class DataTablePanelTest(TestCase):
@@ -148,3 +151,52 @@ class BaseTextPanelTest(TestCase):
148
151
  panel = BaseTextPanel(weight=100)
149
152
  with self.assertRaises(NotImplementedError):
150
153
  panel.get_value({})
154
+
155
+
156
+ class ObjectsTablePanelTest(TestCase):
157
+ def setUp(self):
158
+ super().setUp()
159
+ self.factory = RequestFactory()
160
+ self.request = self.factory.get("/")
161
+ self.request.user = self.user
162
+
163
+ def test_include_exclude_columns(self):
164
+ panel = ObjectsTablePanel(
165
+ weight=100,
166
+ table_class=DeviceTable,
167
+ table_attribute="devices_sorted",
168
+ related_field_name="device_redundancy_group",
169
+ include_columns=[
170
+ "device_redundancy_group_priority",
171
+ ],
172
+ exclude_columns=[
173
+ "rack",
174
+ ],
175
+ )
176
+ redundancy_group = DeviceRedundancyGroup.objects.first()
177
+ context = {
178
+ "request": self.request,
179
+ "object": redundancy_group,
180
+ }
181
+ result = panel.get_extra_context(context)
182
+ columns = result["body_content_table"].columns
183
+ self.assertIn("device_redundancy_group_priority", [col.name for col in columns])
184
+ self.assertNotIn("rack", [col.name for col in columns])
185
+
186
+ def test_invalid_include_columns(self):
187
+ with self.assertRaises(ValueError) as context:
188
+ panel = ObjectsTablePanel(
189
+ weight=100,
190
+ table_class=DeviceTable,
191
+ table_attribute="devices_sorted",
192
+ related_field_name="device_redundancy_group",
193
+ include_columns=["non_existent_column"],
194
+ )
195
+ redundancy_group = DeviceRedundancyGroup.objects.first()
196
+ context_data = {
197
+ "request": self.request,
198
+ "object": redundancy_group,
199
+ }
200
+ panel.get_extra_context(context_data)
201
+
202
+ self.assertIn("non-existent column `non_existent_column`", str(context.exception))
@@ -714,6 +714,17 @@ class LookupRelatedFunctionTest(TestCase):
714
714
  form_field.queryset, extras_utils.ChangeLoggedModelsQuery().as_queryset()
715
715
  )
716
716
 
717
+ form_field = filtering.get_filterset_parameter_form_field(
718
+ extras_models.ObjectMetadata, "assigned_object_type"
719
+ )
720
+ self.assertIsInstance(form_field, forms.MultipleContentTypeField)
721
+ self.assertQuerysetEqualAndNotEmpty(
722
+ form_field.queryset,
723
+ ContentType.objects.filter(extras_utils.FeatureQuery("metadata").get_query()).order_by(
724
+ "app_label", "model"
725
+ ),
726
+ )
727
+
717
728
  with self.subTest("Test prefers_id"):
718
729
  form_field = filtering.get_filterset_parameter_form_field(dcim_models.Device, "location")
719
730
  self.assertEqual("id", form_field.to_field_name)
@@ -1,5 +1,7 @@
1
1
  import json
2
+ import os
2
3
  import re
4
+ import tempfile
3
5
  from unittest import mock, skipIf
4
6
  import urllib.parse
5
7
 
@@ -185,6 +187,77 @@ class HomeViewTestCase(TestCase):
185
187
  self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
186
188
 
187
189
 
190
+ class MediaViewTestCase(TestCase):
191
+ def test_media_unauthenticated(self):
192
+ """
193
+ Test that unauthenticated users are redirected to login when accessing media files whether they exist or not.
194
+ """
195
+ with tempfile.TemporaryDirectory() as temp_dir:
196
+ with override_settings(
197
+ MEDIA_ROOT=temp_dir,
198
+ BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")},
199
+ ):
200
+ file_path = os.path.join(temp_dir, "foo.txt")
201
+ url = reverse("media", kwargs={"path": "foo.txt"})
202
+ self.client.logout()
203
+
204
+ # Unauthenticated request to nonexistent media file should redirect to login page
205
+ response = self.client.get(url)
206
+ self.assertRedirects(
207
+ response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200
208
+ )
209
+
210
+ # Unauthenticated request to existent media file should redirect to login page as well
211
+ with open(file_path, "w") as f:
212
+ f.write("Hello, world!")
213
+ response = self.client.get(url)
214
+ self.assertRedirects(
215
+ response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200
216
+ )
217
+
218
+ def test_branding_media(self):
219
+ """
220
+ Test that users can access branding files listed in `settings.BRANDING_FILEPATHS` regardless of authentication.
221
+ """
222
+ with tempfile.TemporaryDirectory() as temp_dir:
223
+ with override_settings(
224
+ MEDIA_ROOT=temp_dir,
225
+ BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")},
226
+ ):
227
+ os.makedirs(os.path.join(temp_dir, "branding"))
228
+ file_path = os.path.join(temp_dir, "branding", "logo.txt")
229
+ with open(file_path, "w") as f:
230
+ f.write("Hello, world!")
231
+
232
+ url = reverse("media", kwargs={"path": "branding/logo.txt"})
233
+
234
+ # Authenticated request succeeds
235
+ response = self.client.get(url)
236
+ self.assertHttpStatus(response, 200)
237
+ self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
238
+
239
+ # Unauthenticated request also succeeds
240
+ self.client.logout()
241
+ response = self.client.get(url)
242
+ self.assertHttpStatus(response, 200)
243
+ self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
244
+
245
+ def test_media_authenticated(self):
246
+ """
247
+ Test that authenticated users can access regular media files stored in the `MEDIA_ROOT`.
248
+ """
249
+ with tempfile.TemporaryDirectory() as temp_dir:
250
+ with override_settings(MEDIA_ROOT=temp_dir):
251
+ file_path = os.path.join(temp_dir, "foo.txt")
252
+ with open(file_path, "w") as f:
253
+ f.write("Hello, world!")
254
+
255
+ url = reverse("media", kwargs={"path": "foo.txt"})
256
+ response = self.client.get(url)
257
+ self.assertHttpStatus(response, 200)
258
+ self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
259
+
260
+
188
261
  @override_settings(BRANDING_TITLE="Nautobot")
189
262
  class SearchFieldsTestCase(TestCase):
190
263
  def test_search_bar_redirect_to_login(self):
@@ -728,9 +728,7 @@ class ObjectsTablePanel(Panel):
728
728
  table_title (str, optional): The title to display in the panel heading for the table.
729
729
  If None, defaults to the plural verbose name of the table model.
730
730
  include_columns (list, optional): A list of field names to include in the table display.
731
- If provided, only these fields will be displayed in the table.
732
731
  exclude_columns (list, optional): A list of field names to exclude from the table display.
733
- Mutually exclusive with `include_columns`.
734
732
  add_button_route (str, optional): The route used to generate the "add" button URL. Defaults to "default",
735
733
  which uses the default table's model `add` route.
736
734
  add_permissions (list, optional): A list of permissions required for the "add" button to be displayed.
@@ -777,8 +775,6 @@ class ObjectsTablePanel(Panel):
777
775
  self.order_by_fields = order_by_fields
778
776
  self.table_title = table_title
779
777
  self.max_display_count = max_display_count
780
- if exclude_columns and include_columns:
781
- raise ValueError("You can only specify either `exclude_columns` or `include_columns`")
782
778
  self.include_columns = include_columns
783
779
  self.exclude_columns = exclude_columns
784
780
  self.add_button_route = add_button_route
@@ -878,14 +874,17 @@ class ObjectsTablePanel(Panel):
878
874
  # to redirect the user back to the correct tab after editing/deleteing an object
879
875
  body_content_table.columns["actions"].column.extra_context["return_url_extra"] = f"?tab={self.tab_id}"
880
876
 
881
- if self.exclude_columns or self.include_columns:
877
+ if self.exclude_columns:
882
878
  for column in body_content_table.columns:
883
- if (self.exclude_columns and column.name in self.exclude_columns) or (
884
- self.include_columns and column.name not in self.include_columns
885
- ):
879
+ if column.name in self.exclude_columns:
886
880
  body_content_table.columns.hide(column.name)
887
- else:
888
- body_content_table.columns.show(column.name)
881
+
882
+ if self.include_columns:
883
+ for column in self.include_columns:
884
+ if column not in body_content_table.base_columns:
885
+ raise ValueError(f"You are specifying a non-existent column `{column}`")
886
+ body_content_table.columns.show(column)
887
+
889
888
  # Enable bulk action toggle if the user has appropriate permissions
890
889
  user = request.user
891
890
  if self.enable_bulk_actions and (
@@ -906,10 +905,18 @@ class ObjectsTablePanel(Panel):
906
905
  body_content_table_model = body_content_table.Meta.model
907
906
  related_field_name = self.related_field_name or self.table_filter or obj._meta.model_name
908
907
 
908
+ list_url = getattr(self.table_class, "list_url", None)
909
+ if not list_url:
910
+ list_url = get_route_for_model(body_content_table_model, "list")
911
+
909
912
  try:
910
- list_route = reverse(get_route_for_model(body_content_table_model, "list"))
911
- body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
913
+ list_route = reverse(list_url)
912
914
  except NoReverseMatch:
915
+ list_route = None
916
+
917
+ if list_route:
918
+ body_content_table_list_url = f"{list_route}?{related_field_name}={obj.pk}"
919
+ else:
913
920
  body_content_table_list_url = None
914
921
 
915
922
  body_content_table_add_url = self._get_table_add_url(context)
nautobot/core/urls.py CHANGED
@@ -2,13 +2,13 @@ from django.conf import settings
2
2
  from django.http import HttpResponse, HttpResponseNotFound
3
3
  from django.urls import include, path
4
4
  from django.views.generic import TemplateView
5
- from django.views.static import serve
6
5
 
7
6
  from nautobot.core.views import (
8
7
  AboutView,
9
8
  CustomGraphQLView,
10
9
  get_file_with_authorization,
11
10
  HomeView,
11
+ MediaView,
12
12
  NautobotMetricsView,
13
13
  NautobotMetricsViewAuth,
14
14
  RenderJinjaView,
@@ -51,7 +51,7 @@ urlpatterns = [
51
51
  # GraphQL
52
52
  path("graphql/", CustomGraphQLView.as_view(graphiql=True), name="graphql"),
53
53
  # Serving static media in Django (TODO: should be DEBUG mode only - "This view is NOT hardened for production use")
54
- path("media/<path:path>", serve, {"document_root": settings.MEDIA_ROOT}),
54
+ path("media/<path:path>", MediaView.as_view(), name="media"),
55
55
  # Admin
56
56
  path("admin/", admin_site.urls),
57
57
  # Errors
@@ -135,11 +135,14 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
135
135
  from nautobot.core.models.fields import slugify_dashes_to_underscores # Avoid circular import
136
136
 
137
137
  plural_name = slugify_dashes_to_underscores(model._meta.verbose_name_plural)
138
+
138
139
  # Cable-connectable models use "cable_terminations", not "cables", as the feature name
139
140
  if plural_name == "cables":
140
141
  plural_name = "cable_terminations"
141
142
  elif plural_name == "metadata_types":
142
143
  plural_name = "metadata"
144
+ elif plural_name == "object_metadata":
145
+ plural_name = "metadata"
143
146
  try:
144
147
  form_field = MultipleContentTypeField(choices_as_strings=True, feature=plural_name)
145
148
  except KeyError:
@@ -3,6 +3,7 @@ import datetime
3
3
  import logging
4
4
  import os
5
5
  import platform
6
+ import posixpath
6
7
  import re
7
8
  import sys
8
9
  import time
@@ -24,6 +25,7 @@ from django.views.csrf import csrf_failure as _csrf_failure
24
25
  from django.views.decorators.csrf import requires_csrf_token
25
26
  from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
26
27
  from django.views.generic import TemplateView, View
28
+ from django.views.static import serve
27
29
  from graphene_django.views import GraphQLView
28
30
  from packaging import version
29
31
  from prometheus_client import (
@@ -133,6 +135,25 @@ class HomeView(AccessMixin, TemplateView):
133
135
  return self.render_to_response(context)
134
136
 
135
137
 
138
+ class MediaView(AccessMixin, View):
139
+ """
140
+ Serves media files while enforcing login restrictions.
141
+
142
+ This view wraps Django's `serve()` function to ensure that access to media files (with the exception of
143
+ branding files defined in `settings.BRANDING_FILEPATHS`) is restricted to authenticated users.
144
+ """
145
+
146
+ def get(self, request, path):
147
+ if request.user.is_authenticated:
148
+ return serve(request, path, document_root=settings.MEDIA_ROOT)
149
+
150
+ # Unauthenticated users can access BRANDING_FILEPATHS only
151
+ if posixpath.normpath(path).lstrip("/") in settings.BRANDING_FILEPATHS.values():
152
+ return serve(request, path, document_root=settings.MEDIA_ROOT)
153
+
154
+ return self.handle_no_permission()
155
+
156
+
136
157
  class WorkerStatusView(UserPassesTestMixin, TemplateView):
137
158
  template_name = "utilities/worker_status.html"
138
159
 
@@ -301,7 +301,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
301
301
  # If the view does not have a object_detail_content attribute, set it to None.
302
302
  context["object_detail_content"] = None
303
303
  context.update(common_detail_view_context(request, instance))
304
- elif view.action == "list":
304
+ if view.action == "list":
305
305
  # Construct valid actions for list view.
306
306
  valid_actions = self.validate_action_buttons(view, request)
307
307
  # Query SavedViews for dropdown button
nautobot/dcim/forms.py CHANGED
@@ -727,6 +727,16 @@ class RackReservationForm(NautobotModelForm, TenancyForm):
727
727
  "tags",
728
728
  ]
729
729
 
730
+ def __init__(self, *args, **kwargs):
731
+ super().__init__(*args, **kwargs)
732
+
733
+ rack = getattr(self.instance, "rack", None)
734
+ if rack:
735
+ if not self.initial.get("location"):
736
+ self.initial["location"] = rack.location
737
+ if not self.initial.get("rack_group"):
738
+ self.initial["rack_group"] = rack.rack_group
739
+
730
740
 
731
741
  class RackReservationBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
732
742
  pk = forms.ModelMultipleChoiceField(queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput())