nautobot 2.4.9__py3-none-any.whl → 2.4.11__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 (433) hide show
  1. nautobot/cloud/tests/test_views.py +13 -1
  2. nautobot/cloud/views.py +39 -9
  3. nautobot/core/api/parsers.py +56 -2
  4. nautobot/core/celery/__init__.py +21 -0
  5. nautobot/core/celery/encoders.py +3 -0
  6. nautobot/core/forms/forms.py +4 -1
  7. nautobot/core/jobs/bulk_actions.py +8 -8
  8. nautobot/core/jobs/cleanup.py +11 -0
  9. nautobot/core/management/commands/generate_test_data.py +2 -1
  10. nautobot/core/models/__init__.py +2 -0
  11. nautobot/core/templates/generic/object_retrieve.html +1 -1
  12. nautobot/core/testing/mixins.py +19 -1
  13. nautobot/core/testing/views.py +104 -8
  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 +20 -4
  17. nautobot/core/tests/test_utils.py +193 -0
  18. nautobot/core/tests/test_views.py +73 -0
  19. nautobot/core/tests/test_views_utils.py +53 -2
  20. nautobot/core/ui/object_detail.py +4 -0
  21. nautobot/core/urls.py +2 -2
  22. nautobot/core/utils/lookup.py +4 -2
  23. nautobot/core/utils/module_loading.py +86 -58
  24. nautobot/core/views/__init__.py +21 -0
  25. nautobot/core/views/generic.py +2 -12
  26. nautobot/core/views/mixins.py +19 -1
  27. nautobot/core/views/renderers.py +4 -13
  28. nautobot/core/views/utils.py +16 -0
  29. nautobot/dcim/api/serializers.py +13 -0
  30. nautobot/dcim/api/urls.py +1 -0
  31. nautobot/dcim/api/views.py +20 -0
  32. nautobot/dcim/apps.py +1 -0
  33. nautobot/dcim/factory.py +11 -0
  34. nautobot/dcim/filters/__init__.py +110 -0
  35. nautobot/dcim/forms.py +205 -19
  36. nautobot/dcim/migrations/0070_modulefamily_models.py +92 -0
  37. nautobot/dcim/models/__init__.py +2 -0
  38. nautobot/dcim/models/device_component_templates.py +18 -0
  39. nautobot/dcim/models/device_components.py +25 -1
  40. nautobot/dcim/models/devices.py +68 -0
  41. nautobot/dcim/navigation.py +16 -0
  42. nautobot/dcim/tables/__init__.py +2 -0
  43. nautobot/dcim/tables/devices.py +48 -0
  44. nautobot/dcim/tables/devicetypes.py +35 -1
  45. nautobot/dcim/tables/template_code.py +2 -0
  46. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -90
  47. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  48. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +1 -63
  49. nautobot/dcim/templates/dcim/location.html +2 -249
  50. nautobot/dcim/templates/dcim/location_edit.html +2 -38
  51. nautobot/dcim/templates/dcim/location_retrieve.html +249 -0
  52. nautobot/dcim/templates/dcim/location_update.html +38 -0
  53. nautobot/dcim/templates/dcim/module_update.html +1 -0
  54. nautobot/dcim/templates/dcim/modulebay_retrieve.html +93 -1
  55. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +31 -0
  56. nautobot/dcim/templates/dcim/moduletype_retrieve.html +6 -0
  57. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -160
  58. nautobot/dcim/tests/test_api.py +35 -0
  59. nautobot/dcim/tests/test_filters.py +102 -3
  60. nautobot/dcim/tests/test_models.py +146 -0
  61. nautobot/dcim/tests/test_views.py +70 -97
  62. nautobot/dcim/urls.py +4 -22
  63. nautobot/dcim/views.py +439 -153
  64. nautobot/extras/api/views.py +9 -2
  65. nautobot/extras/context_managers.py +2 -2
  66. nautobot/extras/datasources/git.py +11 -3
  67. nautobot/extras/forms/forms.py +9 -5
  68. nautobot/extras/jobs.py +4 -2
  69. nautobot/extras/models/customfields.py +2 -0
  70. nautobot/extras/models/datasources.py +13 -8
  71. nautobot/extras/models/groups.py +18 -0
  72. nautobot/extras/models/jobs.py +19 -0
  73. nautobot/extras/models/metadata.py +2 -0
  74. nautobot/extras/models/models.py +4 -0
  75. nautobot/extras/models/secrets.py +7 -0
  76. nautobot/extras/plugins/__init__.py +3 -0
  77. nautobot/extras/secrets/__init__.py +14 -0
  78. nautobot/extras/tables.py +40 -3
  79. nautobot/extras/templates/extras/configcontext.html +2 -220
  80. nautobot/extras/templates/extras/configcontext_edit.html +2 -50
  81. nautobot/extras/templates/extras/configcontext_retrieve.html +2 -0
  82. nautobot/extras/templates/extras/configcontext_update.html +50 -0
  83. nautobot/extras/templates/extras/configcontextschema.html +2 -48
  84. nautobot/extras/templates/extras/configcontextschema_edit.html +2 -19
  85. nautobot/extras/templates/extras/configcontextschema_retrieve.html +48 -0
  86. nautobot/extras/templates/extras/configcontextschema_update.html +19 -0
  87. nautobot/extras/templates/extras/inc/configcontext_data.html +1 -0
  88. nautobot/extras/templates/extras/inc/json_data.html +1 -1
  89. nautobot/extras/templates/extras/inc/json_format.html +2 -2
  90. nautobot/extras/templates/extras/job_edit.html +12 -6
  91. nautobot/extras/templates/extras/tag.html +2 -52
  92. nautobot/extras/templates/extras/tag_edit.html +2 -15
  93. nautobot/extras/templates/extras/tag_retrieve.html +52 -0
  94. nautobot/extras/templates/extras/tag_update.html +15 -0
  95. nautobot/extras/templates/extras/team_retrieve.html +2 -2
  96. nautobot/extras/tests/test_api.py +15 -15
  97. nautobot/extras/tests/test_context_managers.py +20 -0
  98. nautobot/extras/tests/test_filters.py +4 -4
  99. nautobot/extras/tests/test_jobs.py +23 -10
  100. nautobot/extras/tests/test_models.py +45 -8
  101. nautobot/extras/tests/test_plugins.py +6 -3
  102. nautobot/extras/tests/test_views.py +66 -11
  103. nautobot/extras/urls.py +4 -134
  104. nautobot/extras/views.py +113 -158
  105. nautobot/ipam/models.py +51 -4
  106. nautobot/ipam/tables.py +19 -0
  107. nautobot/ipam/templates/ipam/vlan.html +2 -84
  108. nautobot/ipam/templates/ipam/vlan_edit.html +2 -24
  109. nautobot/ipam/templates/ipam/vlan_retrieve.html +84 -0
  110. nautobot/ipam/templates/ipam/vlan_update.html +24 -0
  111. nautobot/ipam/tests/test_views.py +5 -0
  112. nautobot/ipam/urls.py +1 -21
  113. nautobot/ipam/views.py +45 -70
  114. nautobot/project-static/docs/404.html +31 -8
  115. nautobot/project-static/docs/apps/index.html +31 -8
  116. nautobot/project-static/docs/apps/nautobot-apps.html +31 -8
  117. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +31 -8
  118. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +31 -8
  119. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +31 -8
  120. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +31 -8
  121. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +31 -8
  122. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +31 -8
  123. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +31 -8
  124. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +31 -8
  125. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +31 -8
  126. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +31 -8
  127. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +31 -8
  128. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +31 -8
  129. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +31 -8
  130. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +31 -8
  131. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +31 -8
  132. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +31 -8
  133. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +31 -8
  134. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +31 -8
  135. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +31 -8
  136. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +120 -8
  137. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +31 -8
  138. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +31 -8
  139. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +31 -8
  140. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +31 -8
  141. nautobot/project-static/docs/development/apps/api/configuration-view.html +31 -8
  142. nautobot/project-static/docs/development/apps/api/database-backend-config.html +31 -8
  143. nautobot/project-static/docs/development/apps/api/models/django-admin.html +31 -8
  144. nautobot/project-static/docs/development/apps/api/models/global-search.html +31 -8
  145. nautobot/project-static/docs/development/apps/api/models/graphql.html +31 -8
  146. nautobot/project-static/docs/development/apps/api/models/index.html +31 -8
  147. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +40 -8
  148. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +31 -8
  149. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +31 -8
  150. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +31 -8
  151. nautobot/project-static/docs/development/apps/api/platform-features/index.html +31 -8
  152. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +31 -8
  153. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +31 -8
  154. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +31 -8
  155. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +70 -46
  156. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +31 -8
  157. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +31 -8
  158. nautobot/project-static/docs/development/apps/api/prometheus.html +31 -8
  159. nautobot/project-static/docs/development/apps/api/setup.html +31 -8
  160. nautobot/project-static/docs/development/apps/api/testing.html +31 -8
  161. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +31 -8
  162. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +31 -8
  163. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +31 -8
  164. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +31 -8
  165. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +31 -8
  166. nautobot/project-static/docs/development/apps/api/views/base-template.html +31 -8
  167. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +31 -8
  168. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +31 -8
  169. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +31 -8
  170. nautobot/project-static/docs/development/apps/api/views/index.html +31 -8
  171. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +31 -8
  172. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -8
  173. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +31 -8
  174. nautobot/project-static/docs/development/apps/api/views/notes.html +31 -8
  175. nautobot/project-static/docs/development/apps/api/views/rest-api.html +31 -8
  176. nautobot/project-static/docs/development/apps/api/views/urls.html +31 -8
  177. nautobot/project-static/docs/development/apps/index.html +31 -8
  178. nautobot/project-static/docs/development/apps/migration/code-updates.html +31 -8
  179. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +31 -8
  180. nautobot/project-static/docs/development/apps/migration/from-v1.html +31 -8
  181. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +31 -8
  182. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +31 -8
  183. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +31 -8
  184. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +31 -8
  185. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +31 -8
  186. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +31 -8
  187. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +31 -8
  188. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +31 -8
  189. nautobot/project-static/docs/development/apps/porting-from-netbox.html +31 -8
  190. nautobot/project-static/docs/development/core/application-registry.html +31 -8
  191. nautobot/project-static/docs/development/core/best-practices.html +31 -8
  192. nautobot/project-static/docs/development/core/bootstrap-ui.html +31 -8
  193. nautobot/project-static/docs/development/core/caching.html +31 -8
  194. nautobot/project-static/docs/development/core/controllers.html +31 -8
  195. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +31 -8
  196. nautobot/project-static/docs/development/core/generic-views.html +31 -8
  197. nautobot/project-static/docs/development/core/getting-started.html +31 -8
  198. nautobot/project-static/docs/development/core/homepage.html +31 -8
  199. nautobot/project-static/docs/development/core/index.html +31 -8
  200. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +31 -8
  201. nautobot/project-static/docs/development/core/model-checklist.html +31 -8
  202. nautobot/project-static/docs/development/core/model-features.html +31 -8
  203. nautobot/project-static/docs/development/core/natural-keys.html +31 -8
  204. nautobot/project-static/docs/development/core/navigation-menu.html +31 -8
  205. nautobot/project-static/docs/development/core/release-checklist.html +31 -8
  206. nautobot/project-static/docs/development/core/role-internals.html +31 -8
  207. nautobot/project-static/docs/development/core/settings.html +31 -8
  208. nautobot/project-static/docs/development/core/style-guide.html +31 -8
  209. nautobot/project-static/docs/development/core/templates.html +31 -8
  210. nautobot/project-static/docs/development/core/testing.html +31 -8
  211. nautobot/project-static/docs/development/core/ui-component-framework.html +31 -8
  212. nautobot/project-static/docs/development/core/user-preferences.html +31 -8
  213. nautobot/project-static/docs/development/index.html +31 -8
  214. nautobot/project-static/docs/development/jobs/getting-started.html +35 -8
  215. nautobot/project-static/docs/development/jobs/index.html +31 -8
  216. nautobot/project-static/docs/development/jobs/installation.html +31 -8
  217. nautobot/project-static/docs/development/jobs/job-extensions.html +31 -8
  218. nautobot/project-static/docs/development/jobs/job-logging.html +31 -8
  219. nautobot/project-static/docs/development/jobs/job-patterns.html +31 -8
  220. nautobot/project-static/docs/development/jobs/job-structure.html +31 -8
  221. nautobot/project-static/docs/development/jobs/migration/from-v1.html +31 -8
  222. nautobot/project-static/docs/development/jobs/testing.html +31 -8
  223. nautobot/project-static/docs/index.html +31 -8
  224. nautobot/project-static/docs/insert-analytics.sh +36 -0
  225. nautobot/project-static/docs/objects.inv +0 -0
  226. nautobot/project-static/docs/overview/application_stack.html +31 -8
  227. nautobot/project-static/docs/overview/design_philosophy.html +31 -8
  228. nautobot/project-static/docs/release-notes/index.html +31 -8
  229. nautobot/project-static/docs/release-notes/version-1.0.html +31 -8
  230. nautobot/project-static/docs/release-notes/version-1.1.html +31 -8
  231. nautobot/project-static/docs/release-notes/version-1.2.html +31 -8
  232. nautobot/project-static/docs/release-notes/version-1.3.html +31 -8
  233. nautobot/project-static/docs/release-notes/version-1.4.html +31 -8
  234. nautobot/project-static/docs/release-notes/version-1.5.html +31 -8
  235. nautobot/project-static/docs/release-notes/version-1.6.html +328 -8
  236. nautobot/project-static/docs/release-notes/version-2.0.html +31 -8
  237. nautobot/project-static/docs/release-notes/version-2.1.html +31 -8
  238. nautobot/project-static/docs/release-notes/version-2.2.html +31 -8
  239. nautobot/project-static/docs/release-notes/version-2.3.html +31 -8
  240. nautobot/project-static/docs/release-notes/version-2.4.html +353 -8
  241. nautobot/project-static/docs/search/search_index.json +1 -1
  242. nautobot/project-static/docs/sitemap.xml +302 -298
  243. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  244. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +31 -8
  245. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +31 -8
  246. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +31 -8
  247. nautobot/project-static/docs/user-guide/administration/configuration/index.html +31 -8
  248. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +31 -8
  249. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +31 -8
  250. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +31 -8
  251. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +31 -8
  252. nautobot/project-static/docs/user-guide/administration/guides/docker.html +31 -8
  253. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +31 -8
  254. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +31 -8
  255. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +31 -8
  256. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +31 -8
  257. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +31 -8
  258. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +31 -8
  259. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +31 -8
  260. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +31 -8
  261. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +31 -8
  262. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +31 -8
  263. nautobot/project-static/docs/user-guide/administration/installation/index.html +31 -8
  264. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +31 -8
  265. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +31 -8
  266. nautobot/project-static/docs/user-guide/administration/installation/services.html +31 -8
  267. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +31 -8
  268. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +31 -8
  269. nautobot/project-static/docs/user-guide/administration/security/index.html +31 -9
  270. nautobot/project-static/docs/user-guide/administration/security/notices.html +144 -9
  271. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +31 -8
  272. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +31 -8
  273. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +31 -8
  274. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +31 -8
  275. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +31 -8
  276. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +31 -8
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +31 -8
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +31 -8
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +31 -8
  280. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +31 -8
  281. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +31 -8
  282. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +31 -8
  283. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +31 -8
  284. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +31 -8
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +31 -8
  286. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +31 -8
  287. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +31 -8
  288. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +31 -8
  289. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +31 -8
  290. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +31 -8
  291. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +31 -8
  292. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +31 -8
  293. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +31 -8
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +31 -8
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +31 -8
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +31 -8
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +31 -8
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +31 -8
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +31 -8
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +31 -8
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +31 -8
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +31 -8
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +31 -8
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +31 -8
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +43 -20
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +31 -8
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +31 -8
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +31 -8
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +31 -8
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +31 -8
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +31 -8
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +31 -8
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +31 -8
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +31 -8
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +31 -8
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +35 -8
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +35 -8
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +35 -8
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +10261 -0
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +34 -11
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +31 -8
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +31 -8
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +31 -8
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +31 -8
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +31 -8
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +31 -8
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +31 -8
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +31 -8
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +31 -8
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +31 -8
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +31 -8
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +31 -8
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +31 -8
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +31 -8
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +31 -8
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +31 -8
  337. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +31 -8
  338. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +31 -8
  339. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +31 -8
  340. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +31 -8
  341. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +31 -8
  342. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +31 -8
  343. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +31 -8
  344. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +31 -8
  345. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +31 -8
  346. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +31 -8
  347. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +31 -8
  348. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +31 -8
  349. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +31 -8
  350. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +31 -8
  351. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +31 -8
  352. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +31 -8
  353. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +31 -8
  354. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +31 -8
  355. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +31 -8
  356. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +31 -8
  357. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +31 -8
  358. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +31 -8
  359. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +31 -8
  360. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +31 -8
  361. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +31 -8
  362. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +31 -8
  363. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +31 -8
  364. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +31 -8
  365. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +31 -8
  366. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +31 -8
  367. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +31 -8
  368. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +31 -8
  369. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +31 -8
  370. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +31 -8
  371. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +31 -8
  372. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +31 -8
  373. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +41 -15
  374. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +31 -8
  375. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +31 -8
  376. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +31 -8
  377. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +31 -8
  378. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +31 -8
  379. nautobot/project-static/docs/user-guide/index.html +31 -8
  380. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +31 -8
  381. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +31 -8
  382. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +31 -8
  383. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +31 -8
  384. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +31 -8
  385. nautobot/project-static/docs/user-guide/platform-functionality/events.html +31 -8
  386. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +31 -8
  387. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +31 -8
  388. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +37 -9
  389. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +31 -8
  390. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +31 -8
  391. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +31 -8
  392. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +31 -8
  393. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +31 -8
  394. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +31 -8
  395. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +31 -8
  396. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +31 -8
  397. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +31 -8
  398. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +31 -8
  399. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +31 -8
  400. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +31 -8
  401. nautobot/project-static/docs/user-guide/platform-functionality/note.html +31 -8
  402. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +31 -8
  403. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +31 -8
  404. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +31 -8
  405. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +31 -8
  406. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +31 -8
  407. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +31 -8
  408. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +31 -8
  409. nautobot/project-static/docs/user-guide/platform-functionality/role.html +31 -8
  410. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +31 -8
  411. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +31 -8
  412. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +31 -8
  413. nautobot/project-static/docs/user-guide/platform-functionality/status.html +31 -8
  414. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +31 -8
  415. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +31 -8
  416. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +31 -8
  417. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +31 -8
  418. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +31 -8
  419. nautobot/tenancy/tables.py +2 -0
  420. nautobot/users/models.py +4 -0
  421. nautobot/virtualization/models.py +4 -0
  422. nautobot/virtualization/tests/test_views.py +1 -1
  423. nautobot/wireless/forms.py +0 -1
  424. nautobot/wireless/models.py +1 -1
  425. nautobot/wireless/tables.py +7 -0
  426. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/METADATA +4 -4
  427. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/RECORD +433 -418
  428. /nautobot/dcim/templates/dcim/{platform_edit.html → platform_create.html} +0 -0
  429. /nautobot/extras/test_jobs/{pass.py → pass_job.py} +0 -0
  430. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/LICENSE.txt +0 -0
  431. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/NOTICE +0 -0
  432. {nautobot-2.4.9.dist-info → nautobot-2.4.11.dist-info}/WHEEL +0 -0
  433. {nautobot-2.4.9.dist-info → nautobot-2.4.11.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)
@@ -689,10 +689,10 @@ class LogsCleanupTestCase(TransactionTestCase):
689
689
  cleanup_types=[CleanupTypes.JOB_RESULT],
690
690
  max_age=60,
691
691
  )
692
- self.assertFalse(JobResult.objects.filter(date_done__lt=cutoff).exists())
693
- self.assertTrue(JobResult.objects.filter(date_done__gte=cutoff).exists())
694
- self.assertTrue(ObjectChange.objects.filter(time__lt=cutoff).exists())
695
- self.assertTrue(ObjectChange.objects.filter(time__gte=cutoff).exists())
692
+ self.assertFalse(JobResult.objects.filter(date_done__lt=cutoff).exists(), cm.output)
693
+ self.assertTrue(JobResult.objects.filter(date_done__gte=cutoff).exists(), cm.output)
694
+ self.assertTrue(ObjectChange.objects.filter(time__lt=cutoff).exists(), cm.output)
695
+ self.assertTrue(ObjectChange.objects.filter(time__gte=cutoff).exists(), cm.output)
696
696
 
697
697
  started_logs = {
698
698
  "job_result_id": str(job_result.id),
@@ -840,6 +840,22 @@ class BulkEditTestCase(TransactionTestCase):
840
840
  )
841
841
  self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), color="aa1409")
842
842
 
843
+ def test_bulk_edit_objects_nullify(self):
844
+ """
845
+ Bulk edit Role instances to nullify their weight.
846
+ """
847
+ self.add_permissions("extras.change_role", "extras.view_role")
848
+ job_result = create_job_result_and_run_job(
849
+ "nautobot.core.jobs.bulk_actions",
850
+ "BulkEditObjects",
851
+ content_type=self.role_ct.id,
852
+ edit_all=True,
853
+ filter_query_params={},
854
+ form_data={"_nullify": ["weight"]},
855
+ username=self.user.username,
856
+ )
857
+ self._common_no_error_test_assertion(Role, job_result, Role.objects.all().count(), weight__isnull=True)
858
+
843
859
  def test_bulk_edit_select_some(self):
844
860
  """
845
861
  Bulk edit selected Namespace instances.
@@ -1,3 +1,7 @@
1
+ import os
2
+ import os.path
3
+ import sys
4
+ import tempfile
1
5
  from unittest import mock
2
6
  import uuid
3
7
 
@@ -17,6 +21,11 @@ from nautobot.core.models import fields as core_fields, utils as models_utils, v
17
21
  from nautobot.core.testing import TestCase
18
22
  from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
19
23
  from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
24
+ from nautobot.core.utils.module_loading import (
25
+ check_name_safe_to_import_privately,
26
+ clear_module_from_sys_modules,
27
+ import_modules_privately,
28
+ )
20
29
  from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
21
30
  from nautobot.extras import models as extras_models, utils as extras_utils
22
31
  from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
@@ -959,6 +968,190 @@ class TestMigrationUtils(TestCase):
959
968
  self.assertEqual(ObjectChange.objects.get(request_id=request_id).related_object_type, location_ct)
960
969
 
961
970
 
971
+ class TestModuleLoadingUtils(TestCase):
972
+ def test_check_name_safe_to_import_privately(self):
973
+ for invalid in (
974
+ "foo.bar", # not a valid identifier
975
+ "😂", # not a valid identifier
976
+ "from", # reserved keyword
977
+ "sys", # Python builtin
978
+ "nautobot", # installed package
979
+ "tkinter", # system library
980
+ ):
981
+ with self.subTest(f"Invalid name: {invalid}"):
982
+ permitted, reason = check_name_safe_to_import_privately(invalid)
983
+ self.assertFalse(permitted)
984
+ self.assertIsInstance(reason, str)
985
+
986
+ def _create_test_files(self, root_directory: str, contents: dict):
987
+ """Helper function to create arbitrary text files in a given directory."""
988
+ for relative_path, file_contents in contents.items():
989
+ os.makedirs(os.path.dirname(os.path.join(root_directory, relative_path)), exist_ok=True)
990
+ with open(os.path.join(root_directory, relative_path), "wt") as fd:
991
+ fd.write(file_contents)
992
+
993
+ def test_import_modules_privately_jobs_root_case(self):
994
+ with tempfile.TemporaryDirectory() as tempdir:
995
+ try:
996
+ contents = {
997
+ # Job file treated as a standalone module
998
+ "some_jobs.py": 'name = "some_jobs"',
999
+ # Job subdirectory treated as a package
1000
+ "my_jobs/__init__.py": '''\
1001
+ import my_jobs.some_submodule
1002
+ from . import relative_submodule
1003
+ name = "my_jobs"''',
1004
+ "my_jobs/some_submodule/__init__.py": 'name = "my_jobs.some_submodule"',
1005
+ "my_jobs/relative_submodule/__init__.py": 'name = "my_jobs.relative_submodule"',
1006
+ # Job file that shouldn't be loaded as it conflicts
1007
+ "tkinter.py": 'name = "tkinter"',
1008
+ # Job submodule that shouldn't be loaded as it conflicts
1009
+ "turtle/__init__.py": 'name = "turtle"',
1010
+ }
1011
+ self._create_test_files(tempdir, contents)
1012
+
1013
+ modules = import_modules_privately(tempdir, ignore_import_errors=False)
1014
+ self.assertEqual(["my_jobs", "some_jobs"], sorted([module.__name__ for module in modules]))
1015
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1016
+ if "some_jobs" not in sys.modules:
1017
+ self.fail("Valid module wasn't loaded from JOBS_ROOT")
1018
+ if "my_jobs" not in sys.modules:
1019
+ self.fail("Valid package wasn't loaded from JOBS_ROOT")
1020
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from JOBS_ROOT"):
1021
+ sys.modules["tkinter"]
1022
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from JOBS_ROOT"):
1023
+ sys.modules["turtle"]
1024
+
1025
+ self.assertEqual(sys.modules["some_jobs"].name, "some_jobs")
1026
+ self.assertEqual(sys.modules["my_jobs"].name, "my_jobs")
1027
+ self.assertEqual(sys.modules["my_jobs"].some_submodule.name, "my_jobs.some_submodule")
1028
+ # self.assertEqual(sys.modules["my_jobs"].relative_submodule.name, "my_jobs.relative_submodule")
1029
+
1030
+ finally:
1031
+ clear_module_from_sys_modules("some_jobs")
1032
+ clear_module_from_sys_modules("my_jobs")
1033
+
1034
+ self.assertNotIn("some_jobs", sys.modules.keys())
1035
+ self.assertNotIn("my_jobs", sys.modules.keys())
1036
+ self.assertNotIn("my_jobs.some_submodule", sys.modules.keys())
1037
+
1038
+ # Test reloading of modules after code changes
1039
+ try:
1040
+ contents["some_jobs.py"] = 'name = "some_jobs_new"'
1041
+ contents["my_jobs/__init__.py"] = '''\
1042
+ import my_jobs.some_submodule
1043
+ from . import relative_submodule
1044
+ name = "my_jobs_new"'''
1045
+ contents["my_jobs/some_submodule/__init__.py"] = 'name = "my_jobs.some_submodule_new"'
1046
+ self._create_test_files(tempdir, contents)
1047
+
1048
+ modules = import_modules_privately(tempdir, ignore_import_errors=False)
1049
+ self.assertEqual(["my_jobs", "some_jobs"], sorted([module.__name__ for module in modules]))
1050
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1051
+ if "some_jobs" not in sys.modules:
1052
+ self.fail("Valid module wasn't loaded from JOBS_ROOT")
1053
+ if "my_jobs" not in sys.modules:
1054
+ self.fail("Valid package wasn't loaded from JOBS_ROOT")
1055
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from JOBS_ROOT"):
1056
+ sys.modules["tkinter"]
1057
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from JOBS_ROOT"):
1058
+ sys.modules["turtle"]
1059
+
1060
+ self.assertEqual(sys.modules["some_jobs"].name, "some_jobs_new")
1061
+ self.assertEqual(sys.modules["my_jobs"].name, "my_jobs_new")
1062
+ self.assertEqual(sys.modules["my_jobs"].some_submodule.name, "my_jobs.some_submodule_new")
1063
+
1064
+ finally:
1065
+ clear_module_from_sys_modules("some_jobs")
1066
+ clear_module_from_sys_modules("my_jobs")
1067
+
1068
+ self.assertNotIn("some_jobs", sys.modules.keys())
1069
+ self.assertNotIn("my_jobs", sys.modules.keys())
1070
+ self.assertNotIn("my_jobs.some_submodule", sys.modules.keys())
1071
+
1072
+ def test_import_modules_privately_git_repo_jobs_case(self):
1073
+ with tempfile.TemporaryDirectory() as tempdir:
1074
+ try:
1075
+ contents = {
1076
+ # Repo that we intend to load
1077
+ "my_repo/__init__.py": 'name = "my_repo"',
1078
+ "my_repo/jobs/__init__.py": '''\
1079
+ import my_repo.jobs.some_jobs
1080
+ from . import some_other_jobs
1081
+ name = "my_repo.jobs"''',
1082
+ "my_repo/jobs/some_jobs.py": 'name = "my_repo.jobs.some_jobs"',
1083
+ "my_repo/jobs/some_other_jobs.py": 'name = "my_repo.jobs.some_other_jobs"',
1084
+ # A separate repo, not intended to be loaded
1085
+ "other_repo/__init__.py": "",
1086
+ # File that shouldn't be loaded as it conflicts
1087
+ "tkinter.py": "",
1088
+ # Package that shouldn't be loaded as it conflicts
1089
+ "turtle/__init__.py": "",
1090
+ }
1091
+ self._create_test_files(tempdir, contents)
1092
+
1093
+ modules = import_modules_privately(tempdir, module_path=["my_repo", "jobs"], ignore_import_errors=False)
1094
+ self.assertEqual(["my_repo", "my_repo.jobs"], sorted([module.__name__ for module in modules]))
1095
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1096
+ if "my_repo" not in sys.modules:
1097
+ self.fail("Valid repo wasn't loaded from GIT_ROOT")
1098
+ if "my_repo.jobs" not in sys.modules:
1099
+ self.fail("Valid repo subdirectory wasn't loaded from GIT_ROOT")
1100
+ with self.assertRaises(KeyError, msg="unexpected repo was loaded from GIT_ROOT"):
1101
+ sys.modules["other_repo"]
1102
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from GIT_ROOT"):
1103
+ sys.modules["tkinter"]
1104
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from GIT_ROOT"):
1105
+ sys.modules["turtle"]
1106
+
1107
+ self.assertEqual(sys.modules["my_repo"].name, "my_repo")
1108
+ self.assertEqual(sys.modules["my_repo.jobs"].name, "my_repo.jobs")
1109
+ self.assertEqual(sys.modules["my_repo.jobs"].some_jobs.name, "my_repo.jobs.some_jobs")
1110
+ self.assertEqual(sys.modules["my_repo.jobs"].some_other_jobs.name, "my_repo.jobs.some_other_jobs")
1111
+
1112
+ finally:
1113
+ clear_module_from_sys_modules("my_repo")
1114
+
1115
+ self.assertNotIn("my_repo", sys.modules.keys())
1116
+ self.assertNotIn("my_repo.jobs", sys.modules.keys())
1117
+
1118
+ # Test reloading of modules after code changes
1119
+ try:
1120
+ contents["my_repo/__init__.py"] = 'name = "my_repo_new"'
1121
+ contents["my_repo/jobs/__init__.py"] = '''\
1122
+ import my_repo.jobs.some_jobs
1123
+ from . import some_other_jobs
1124
+ name = "my_repo.jobs_new"'''
1125
+ contents["my_repo/jobs/some_jobs.py"] = 'name = "my_repo.jobs.some_jobs_new"'
1126
+ contents["my_repo/jobs/some_other_jobs.py"] = 'name = "my_repo.jobs.some_other_jobs_new"'
1127
+ self._create_test_files(tempdir, contents)
1128
+
1129
+ modules = import_modules_privately(tempdir, module_path=["my_repo", "jobs"], ignore_import_errors=False)
1130
+ self.assertEqual(["my_repo", "my_repo.jobs"], sorted([module.__name__ for module in modules]))
1131
+ # assertIn/assertNotIn are super noisy when dealing with the huge sys.modules dict, so instead:
1132
+ if "my_repo" not in sys.modules:
1133
+ self.fail("Valid repo wasn't loaded from GIT_ROOT")
1134
+ if "my_repo.jobs" not in sys.modules:
1135
+ self.fail("Valid repo subdirectory wasn't loaded from GIT_ROOT")
1136
+ with self.assertRaises(KeyError, msg="unexpected repo was loaded from GIT_ROOT"):
1137
+ sys.modules["other_repo"]
1138
+ with self.assertRaises(KeyError, msg="conflicting module name was loaded unsafely from GIT_ROOT"):
1139
+ sys.modules["tkinter"]
1140
+ with self.assertRaises(KeyError, msg="conflicting package name was loaded unsafely from GIT_ROOT"):
1141
+ sys.modules["turtle"]
1142
+
1143
+ self.assertEqual(sys.modules["my_repo"].name, "my_repo_new")
1144
+ self.assertEqual(sys.modules["my_repo.jobs"].name, "my_repo.jobs_new")
1145
+ self.assertEqual(sys.modules["my_repo.jobs"].some_jobs.name, "my_repo.jobs.some_jobs_new")
1146
+ self.assertEqual(sys.modules["my_repo.jobs"].some_other_jobs.name, "my_repo.jobs.some_other_jobs_new")
1147
+
1148
+ finally:
1149
+ clear_module_from_sys_modules("my_repo")
1150
+
1151
+ self.assertNotIn("my_repo", sys.modules.keys())
1152
+ self.assertNotIn("my_repo.jobs", sys.modules.keys())
1153
+
1154
+
962
1155
  class TestQuerySetUtils(TestCase):
963
1156
  def test_maybe_select_related(self):
964
1157
  # If possible, select_related should be called
@@ -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):
@@ -1,13 +1,16 @@
1
1
  import urllib.parse
2
2
 
3
+ from django.contrib.auth.models import AnonymousUser
3
4
  from django.db import ProgrammingError
4
5
  from django.test import TestCase
5
6
 
6
7
  from nautobot.core.models.querysets import count_related
7
- from nautobot.core.views.utils import check_filter_for_display, prepare_cloned_fields
8
+ from nautobot.core.testing import TransactionTestCase
9
+ from nautobot.core.views.utils import check_filter_for_display, get_saved_views_for_user, prepare_cloned_fields
8
10
  from nautobot.dcim.filters import DeviceFilterSet
9
11
  from nautobot.dcim.models import Device, DeviceRedundancyGroup, DeviceType, InventoryItem, Location, Manufacturer
10
- from nautobot.extras.models import Role, Status
12
+ from nautobot.extras.models import Role, SavedView, Status
13
+ from nautobot.users.models import User
11
14
 
12
15
 
13
16
  class CheckFilterForDisplayTest(TestCase):
@@ -168,3 +171,51 @@ class CheckPrepareClonedFields(TestCase):
168
171
  self.assertTrue(isinstance(query_params["description"], list))
169
172
  self.assertTrue(len(query_params["description"]) == 1)
170
173
  self.assertTrue(query_params["description"][0] == description)
174
+
175
+
176
+ class GetSavedViewsForUserTestCase(TransactionTestCase):
177
+ """
178
+ Class to test `get_saved_views_for_user`.
179
+ """
180
+
181
+ def create_saved_view(self, name, owner=None, is_shared=False):
182
+ """Helper to create a SavedView."""
183
+ return SavedView.objects.create(
184
+ name=name, owner=owner or self.user, view="dcim:device_list", is_shared=is_shared
185
+ )
186
+
187
+ def setUp(self):
188
+ super().setUp()
189
+ self.user2 = User.objects.create_user(username="second_user")
190
+ self.create_saved_view(name="saved_view")
191
+ self.create_saved_view(name="saved_view_shared", is_shared=True)
192
+ self.create_saved_view(name="saved_view_different_owner", owner=self.user2)
193
+ self.create_saved_view(name="saved_view_shared_different_owner", is_shared=True, owner=self.user2)
194
+
195
+ def test_user_with_permissions_get_all_saved_views(self):
196
+ """Test if for user with permissions method will return all saved views."""
197
+ self.add_permissions("extras.view_savedview")
198
+ saved_views = get_saved_views_for_user(self.user, "dcim:device_list")
199
+ self.assertEqual(saved_views.count(), 4)
200
+ expected_names = [
201
+ "saved_view",
202
+ "saved_view_different_owner",
203
+ "saved_view_shared",
204
+ "saved_view_shared_different_owner",
205
+ ]
206
+ self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
207
+
208
+ def test_user_without_permissions_get_shared_views_and_own_views_only(self):
209
+ """Test if user without permissions can see shared views and own views."""
210
+ saved_views = get_saved_views_for_user(self.user, "dcim:device_list")
211
+ self.assertEqual(saved_views.count(), 3)
212
+ expected_names = ["saved_view", "saved_view_shared", "saved_view_shared_different_owner"]
213
+ self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
214
+
215
+ def test_anonymous_user_get_shared_views_only(self):
216
+ """Test if method is working with anonymous users and return only shared views."""
217
+ user = AnonymousUser()
218
+ saved_views = get_saved_views_for_user(user, "dcim:device_list")
219
+ self.assertEqual(saved_views.count(), 2)
220
+ expected_names = ["saved_view_shared", "saved_view_shared_different_owner"]
221
+ self.assertEqual(list(saved_views.values_list("name", flat=True)), expected_names)
@@ -1266,6 +1266,10 @@ class ObjectFieldsPanel(KeyValueTablePanel):
1266
1266
 
1267
1267
  data[field_name] = field_value
1268
1268
 
1269
+ # Ensuring the `name` field is displayed first, if present.
1270
+ if "name" in data:
1271
+ data = {"name": data["name"], **{k: v for k, v in data.items() if k != "name"}}
1272
+
1269
1273
  return data
1270
1274
 
1271
1275
  def render_key(self, key, value, context: 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
@@ -314,13 +314,15 @@ def get_created_and_last_updated_usernames_for_model(instance):
314
314
  created_by = None
315
315
  last_updated_by = None
316
316
  try:
317
- created_by_record = object_change_records.filter(action=ObjectChangeActionChoices.ACTION_CREATE).first()
317
+ created_by_record = (
318
+ object_change_records.filter(action=ObjectChangeActionChoices.ACTION_CREATE).only("user_name").first()
319
+ )
318
320
  if created_by_record is not None:
319
321
  created_by = created_by_record.user_name
320
322
  except ObjectChange.DoesNotExist:
321
323
  pass
322
324
 
323
- last_updated_by_record = object_change_records.first()
325
+ last_updated_by_record = object_change_records.only("user_name").first()
324
326
  if last_updated_by_record:
325
327
  last_updated_by = last_updated_by_record.user_name
326
328