nautobot 2.4.10__py3-none-any.whl → 2.4.12__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 (439) hide show
  1. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -114
  2. nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +1 -1
  3. nautobot/circuits/views.py +76 -6
  4. nautobot/cloud/navigation.py +1 -1
  5. nautobot/cloud/tests/test_views.py +13 -1
  6. nautobot/cloud/views.py +39 -9
  7. nautobot/core/celery/__init__.py +21 -0
  8. nautobot/core/celery/encoders.py +3 -0
  9. nautobot/core/forms/forms.py +4 -1
  10. nautobot/core/jobs/bulk_actions.py +8 -8
  11. nautobot/core/jobs/cleanup.py +11 -0
  12. nautobot/core/management/commands/generate_test_data.py +2 -1
  13. nautobot/core/templates/components/panel/header_extra_content_table.html +12 -1
  14. nautobot/core/templates/components/panel/panel.html +4 -0
  15. nautobot/core/templates/generic/object_retrieve.html +2 -1
  16. nautobot/core/testing/mixins.py +19 -1
  17. nautobot/core/testing/views.py +104 -8
  18. nautobot/core/tests/test_jobs.py +20 -4
  19. nautobot/core/tests/test_utils.py +17 -0
  20. nautobot/core/tests/test_views.py +2 -2
  21. nautobot/core/tests/test_views_utils.py +53 -2
  22. nautobot/core/ui/object_detail.py +5 -1
  23. nautobot/core/utils/lookup.py +4 -2
  24. nautobot/core/utils/module_loading.py +23 -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 +116 -0
  35. nautobot/dcim/filters/mixins.py +2 -1
  36. nautobot/dcim/forms.py +205 -19
  37. nautobot/dcim/migrations/0070_modulefamily_models.py +92 -0
  38. nautobot/dcim/models/__init__.py +2 -0
  39. nautobot/dcim/models/device_component_templates.py +14 -0
  40. nautobot/dcim/models/device_components.py +13 -1
  41. nautobot/dcim/models/devices.py +72 -0
  42. nautobot/dcim/navigation.py +16 -0
  43. nautobot/dcim/tables/__init__.py +2 -0
  44. nautobot/dcim/tables/devices.py +50 -0
  45. nautobot/dcim/tables/devicetypes.py +35 -1
  46. nautobot/dcim/tables/template_code.py +2 -0
  47. nautobot/dcim/templates/dcim/controller/base.html +1 -9
  48. nautobot/dcim/templates/dcim/controller_retrieve.html +2 -83
  49. nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +2 -25
  50. nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -90
  51. nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +1 -1
  52. nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +1 -63
  53. nautobot/dcim/templates/dcim/location.html +2 -249
  54. nautobot/dcim/templates/dcim/location_edit.html +2 -38
  55. nautobot/dcim/templates/dcim/location_retrieve.html +249 -0
  56. nautobot/dcim/templates/dcim/location_update.html +38 -0
  57. nautobot/dcim/templates/dcim/module_update.html +1 -0
  58. nautobot/dcim/templates/dcim/modulebay_retrieve.html +93 -1
  59. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +31 -0
  60. nautobot/dcim/templates/dcim/moduletype_retrieve.html +6 -0
  61. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -160
  62. nautobot/dcim/templates/dcim/virtualchassis.html +2 -51
  63. nautobot/dcim/templates/dcim/virtualchassis_add.html +2 -22
  64. nautobot/dcim/templates/dcim/virtualchassis_create.html +22 -0
  65. nautobot/dcim/templates/dcim/virtualchassis_edit.html +2 -93
  66. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +51 -0
  67. nautobot/dcim/templates/dcim/virtualchassis_update.html +93 -0
  68. nautobot/dcim/tests/test_api.py +35 -0
  69. nautobot/dcim/tests/test_filters.py +102 -3
  70. nautobot/dcim/tests/test_models.py +146 -0
  71. nautobot/dcim/tests/test_views.py +70 -97
  72. nautobot/dcim/urls.py +5 -80
  73. nautobot/dcim/views.py +584 -342
  74. nautobot/extras/api/views.py +9 -2
  75. nautobot/extras/datasources/git.py +9 -1
  76. nautobot/extras/forms/forms.py +9 -5
  77. nautobot/extras/jobs.py +4 -2
  78. nautobot/extras/jobs_ui.py +208 -0
  79. nautobot/extras/models/datasources.py +5 -8
  80. nautobot/extras/models/jobs.py +5 -0
  81. nautobot/extras/models/tags.py +4 -0
  82. nautobot/extras/plugins/__init__.py +3 -0
  83. nautobot/extras/tables.py +40 -3
  84. nautobot/extras/templates/extras/configcontext.html +2 -220
  85. nautobot/extras/templates/extras/configcontext_edit.html +2 -50
  86. nautobot/extras/templates/extras/configcontext_retrieve.html +2 -0
  87. nautobot/extras/templates/extras/configcontext_update.html +50 -0
  88. nautobot/extras/templates/extras/configcontextschema.html +2 -48
  89. nautobot/extras/templates/extras/configcontextschema_edit.html +2 -19
  90. nautobot/extras/templates/extras/configcontextschema_retrieve.html +48 -0
  91. nautobot/extras/templates/extras/configcontextschema_update.html +19 -0
  92. nautobot/extras/templates/extras/inc/configcontext_data.html +1 -0
  93. nautobot/extras/templates/extras/inc/configcontext_format.html +6 -0
  94. nautobot/extras/templates/extras/job_detail.html +1 -326
  95. nautobot/extras/templates/extras/job_edit.html +12 -6
  96. nautobot/extras/templates/extras/tag.html +2 -52
  97. nautobot/extras/templates/extras/tag_edit.html +2 -15
  98. nautobot/extras/templates/extras/tag_retrieve.html +2 -0
  99. nautobot/extras/templates/extras/tag_update.html +15 -0
  100. nautobot/extras/templates/extras/team_retrieve.html +2 -2
  101. nautobot/extras/tests/test_api.py +15 -15
  102. nautobot/extras/tests/test_filters.py +4 -4
  103. nautobot/extras/tests/test_jobs.py +23 -10
  104. nautobot/extras/tests/test_models.py +19 -8
  105. nautobot/extras/tests/test_plugins.py +6 -3
  106. nautobot/extras/tests/test_views.py +66 -11
  107. nautobot/extras/urls.py +4 -134
  108. nautobot/extras/views.py +186 -178
  109. nautobot/ipam/models.py +19 -4
  110. nautobot/ipam/tables.py +19 -0
  111. nautobot/ipam/templates/ipam/vlan.html +2 -84
  112. nautobot/ipam/templates/ipam/vlan_edit.html +2 -24
  113. nautobot/ipam/templates/ipam/vlan_retrieve.html +84 -0
  114. nautobot/ipam/templates/ipam/vlan_update.html +24 -0
  115. nautobot/ipam/tests/test_views.py +5 -0
  116. nautobot/ipam/urls.py +1 -21
  117. nautobot/ipam/views.py +45 -70
  118. nautobot/project-static/docs/404.html +33 -10
  119. nautobot/project-static/docs/apps/index.html +33 -10
  120. nautobot/project-static/docs/apps/nautobot-apps.html +33 -10
  121. nautobot/project-static/docs/assets/javascripts/{bundle.13a4f30d.min.js → bundle.56ea9cef.min.js} +2 -2
  122. nautobot/project-static/docs/assets/javascripts/{bundle.13a4f30d.min.js.map → bundle.56ea9cef.min.js.map} +2 -2
  123. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +33 -10
  124. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +33 -10
  125. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +33 -10
  126. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +33 -10
  127. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +33 -10
  128. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +33 -10
  129. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +33 -10
  130. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +33 -10
  131. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +33 -10
  132. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +33 -10
  133. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +33 -10
  134. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +33 -10
  135. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +33 -10
  136. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +33 -10
  137. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +33 -10
  138. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +33 -10
  139. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +33 -10
  140. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +33 -10
  141. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +33 -10
  142. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +122 -10
  143. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +33 -10
  144. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +33 -10
  145. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +33 -10
  146. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +33 -10
  147. nautobot/project-static/docs/development/apps/api/configuration-view.html +33 -10
  148. nautobot/project-static/docs/development/apps/api/database-backend-config.html +33 -10
  149. nautobot/project-static/docs/development/apps/api/models/django-admin.html +33 -10
  150. nautobot/project-static/docs/development/apps/api/models/global-search.html +33 -10
  151. nautobot/project-static/docs/development/apps/api/models/graphql.html +33 -10
  152. nautobot/project-static/docs/development/apps/api/models/index.html +33 -10
  153. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +42 -10
  154. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +33 -10
  155. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +33 -10
  156. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +33 -10
  157. nautobot/project-static/docs/development/apps/api/platform-features/index.html +33 -10
  158. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +33 -10
  159. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +33 -10
  160. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +33 -10
  161. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +34 -11
  162. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +33 -10
  163. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +33 -10
  164. nautobot/project-static/docs/development/apps/api/prometheus.html +33 -10
  165. nautobot/project-static/docs/development/apps/api/setup.html +33 -10
  166. nautobot/project-static/docs/development/apps/api/testing.html +33 -10
  167. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +33 -10
  168. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +33 -10
  169. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +33 -10
  170. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +33 -10
  171. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +33 -10
  172. nautobot/project-static/docs/development/apps/api/views/base-template.html +33 -10
  173. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +33 -10
  174. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +33 -10
  175. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +33 -10
  176. nautobot/project-static/docs/development/apps/api/views/index.html +33 -10
  177. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +33 -10
  178. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +33 -10
  179. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +33 -10
  180. nautobot/project-static/docs/development/apps/api/views/notes.html +33 -10
  181. nautobot/project-static/docs/development/apps/api/views/rest-api.html +33 -10
  182. nautobot/project-static/docs/development/apps/api/views/urls.html +33 -10
  183. nautobot/project-static/docs/development/apps/index.html +33 -10
  184. nautobot/project-static/docs/development/apps/migration/code-updates.html +33 -10
  185. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +33 -10
  186. nautobot/project-static/docs/development/apps/migration/from-v1.html +33 -10
  187. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +33 -10
  188. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +33 -10
  189. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +33 -10
  190. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +33 -10
  191. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +33 -10
  192. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +33 -10
  193. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +33 -10
  194. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +33 -10
  195. nautobot/project-static/docs/development/apps/porting-from-netbox.html +33 -10
  196. nautobot/project-static/docs/development/core/application-registry.html +33 -10
  197. nautobot/project-static/docs/development/core/best-practices.html +33 -10
  198. nautobot/project-static/docs/development/core/bootstrap-ui.html +33 -10
  199. nautobot/project-static/docs/development/core/caching.html +33 -10
  200. nautobot/project-static/docs/development/core/controllers.html +33 -10
  201. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +33 -10
  202. nautobot/project-static/docs/development/core/generic-views.html +33 -10
  203. nautobot/project-static/docs/development/core/getting-started.html +33 -10
  204. nautobot/project-static/docs/development/core/homepage.html +33 -10
  205. nautobot/project-static/docs/development/core/index.html +33 -10
  206. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +33 -10
  207. nautobot/project-static/docs/development/core/model-checklist.html +33 -10
  208. nautobot/project-static/docs/development/core/model-features.html +33 -10
  209. nautobot/project-static/docs/development/core/natural-keys.html +33 -10
  210. nautobot/project-static/docs/development/core/navigation-menu.html +33 -10
  211. nautobot/project-static/docs/development/core/release-checklist.html +33 -10
  212. nautobot/project-static/docs/development/core/role-internals.html +33 -10
  213. nautobot/project-static/docs/development/core/settings.html +33 -10
  214. nautobot/project-static/docs/development/core/style-guide.html +33 -10
  215. nautobot/project-static/docs/development/core/templates.html +33 -10
  216. nautobot/project-static/docs/development/core/testing.html +33 -10
  217. nautobot/project-static/docs/development/core/ui-component-framework.html +33 -10
  218. nautobot/project-static/docs/development/core/user-preferences.html +33 -10
  219. nautobot/project-static/docs/development/index.html +33 -10
  220. nautobot/project-static/docs/development/jobs/getting-started.html +33 -10
  221. nautobot/project-static/docs/development/jobs/index.html +33 -10
  222. nautobot/project-static/docs/development/jobs/installation.html +33 -10
  223. nautobot/project-static/docs/development/jobs/job-extensions.html +33 -10
  224. nautobot/project-static/docs/development/jobs/job-logging.html +33 -10
  225. nautobot/project-static/docs/development/jobs/job-patterns.html +33 -10
  226. nautobot/project-static/docs/development/jobs/job-structure.html +33 -10
  227. nautobot/project-static/docs/development/jobs/migration/from-v1.html +33 -10
  228. nautobot/project-static/docs/development/jobs/testing.html +33 -10
  229. nautobot/project-static/docs/index.html +33 -10
  230. nautobot/project-static/docs/insert-analytics.sh +36 -0
  231. nautobot/project-static/docs/objects.inv +0 -0
  232. nautobot/project-static/docs/overview/application_stack.html +33 -10
  233. nautobot/project-static/docs/overview/design_philosophy.html +33 -10
  234. nautobot/project-static/docs/release-notes/index.html +33 -10
  235. nautobot/project-static/docs/release-notes/version-1.0.html +33 -10
  236. nautobot/project-static/docs/release-notes/version-1.1.html +33 -10
  237. nautobot/project-static/docs/release-notes/version-1.2.html +33 -10
  238. nautobot/project-static/docs/release-notes/version-1.3.html +33 -10
  239. nautobot/project-static/docs/release-notes/version-1.4.html +33 -10
  240. nautobot/project-static/docs/release-notes/version-1.5.html +33 -10
  241. nautobot/project-static/docs/release-notes/version-1.6.html +33 -10
  242. nautobot/project-static/docs/release-notes/version-2.0.html +33 -10
  243. nautobot/project-static/docs/release-notes/version-2.1.html +33 -10
  244. nautobot/project-static/docs/release-notes/version-2.2.html +33 -10
  245. nautobot/project-static/docs/release-notes/version-2.3.html +33 -10
  246. nautobot/project-static/docs/release-notes/version-2.4.html +363 -10
  247. nautobot/project-static/docs/requirements.txt +1 -1
  248. nautobot/project-static/docs/search/search_index.json +1 -1
  249. nautobot/project-static/docs/sitemap.xml +302 -298
  250. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  251. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +33 -10
  252. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +33 -10
  253. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +33 -10
  254. nautobot/project-static/docs/user-guide/administration/configuration/index.html +33 -10
  255. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +33 -10
  256. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +33 -10
  257. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +33 -10
  258. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +33 -10
  259. nautobot/project-static/docs/user-guide/administration/guides/docker.html +33 -10
  260. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +33 -10
  261. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +33 -10
  262. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +33 -10
  263. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +33 -10
  264. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +33 -10
  265. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +33 -10
  266. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +33 -10
  267. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +33 -10
  268. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +33 -10
  269. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +33 -10
  270. nautobot/project-static/docs/user-guide/administration/installation/index.html +33 -10
  271. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +33 -10
  272. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +33 -10
  273. nautobot/project-static/docs/user-guide/administration/installation/services.html +33 -10
  274. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +33 -10
  275. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +33 -10
  276. nautobot/project-static/docs/user-guide/administration/security/index.html +33 -10
  277. nautobot/project-static/docs/user-guide/administration/security/notices.html +33 -10
  278. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +33 -10
  279. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +33 -10
  280. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +33 -10
  281. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +33 -10
  282. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +33 -10
  283. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +33 -10
  284. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +33 -10
  285. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +33 -10
  286. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +33 -10
  287. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +33 -10
  288. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +33 -10
  289. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +33 -10
  290. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +33 -10
  291. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +33 -10
  292. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +33 -10
  293. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +33 -10
  294. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +33 -10
  295. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +33 -10
  296. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +33 -10
  297. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +33 -10
  298. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +33 -10
  299. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +33 -10
  300. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +33 -10
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +33 -10
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +33 -10
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +33 -10
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +33 -10
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +33 -10
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +33 -10
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +33 -10
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +33 -10
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +33 -10
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +33 -10
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +33 -10
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +45 -22
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +33 -10
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +33 -10
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +33 -10
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +33 -10
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +33 -10
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +33 -10
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +33 -10
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +33 -10
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +33 -10
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +33 -10
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +37 -10
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +37 -10
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +37 -10
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +10261 -0
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +36 -13
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +33 -10
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +33 -10
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +33 -10
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +33 -10
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +33 -10
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +33 -10
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +33 -10
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +33 -10
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +33 -10
  337. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +33 -10
  338. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +33 -10
  339. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +33 -10
  340. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +33 -10
  341. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +33 -10
  342. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +33 -10
  343. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +33 -10
  344. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +33 -10
  345. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +33 -10
  346. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +33 -10
  347. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +33 -10
  348. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +33 -10
  349. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +33 -10
  350. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +33 -10
  351. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +33 -10
  352. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +33 -10
  353. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +33 -10
  354. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +33 -10
  355. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +33 -10
  356. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +33 -10
  357. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +33 -10
  358. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +33 -10
  359. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +33 -10
  360. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +33 -10
  361. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +33 -10
  362. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +33 -10
  363. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +33 -10
  364. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +33 -10
  365. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +33 -10
  366. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +33 -10
  367. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +33 -10
  368. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +33 -10
  369. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +33 -10
  370. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +33 -10
  371. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +33 -10
  372. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +33 -10
  373. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +33 -10
  374. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +33 -10
  375. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +33 -10
  376. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +33 -10
  377. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +33 -10
  378. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +33 -10
  379. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +33 -10
  380. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +44 -18
  381. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +33 -10
  382. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +33 -10
  383. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +33 -10
  384. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +33 -10
  385. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +33 -10
  386. nautobot/project-static/docs/user-guide/index.html +33 -10
  387. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +33 -10
  388. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +33 -10
  389. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +33 -10
  390. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +33 -10
  391. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +33 -10
  392. nautobot/project-static/docs/user-guide/platform-functionality/events.html +33 -10
  393. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +33 -10
  394. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +33 -10
  395. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +39 -11
  396. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +33 -10
  397. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +33 -10
  398. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +33 -10
  399. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +33 -10
  400. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +33 -10
  401. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +33 -10
  402. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +33 -10
  403. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +33 -10
  404. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +33 -10
  405. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +33 -10
  406. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +33 -10
  407. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +33 -10
  408. nautobot/project-static/docs/user-guide/platform-functionality/note.html +33 -10
  409. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +33 -10
  410. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +33 -10
  411. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +33 -10
  412. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +33 -10
  413. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +33 -10
  414. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +33 -10
  415. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +33 -10
  416. nautobot/project-static/docs/user-guide/platform-functionality/role.html +33 -10
  417. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +33 -10
  418. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +33 -10
  419. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +33 -10
  420. nautobot/project-static/docs/user-guide/platform-functionality/status.html +33 -10
  421. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +33 -10
  422. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +33 -10
  423. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +33 -10
  424. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +33 -10
  425. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +33 -10
  426. nautobot/tenancy/tables.py +2 -0
  427. nautobot/virtualization/tests/test_views.py +1 -1
  428. nautobot/wireless/forms.py +0 -1
  429. nautobot/wireless/models.py +1 -1
  430. nautobot/wireless/navigation.py +1 -1
  431. nautobot/wireless/tables.py +18 -3
  432. {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/METADATA +4 -4
  433. {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/RECORD +439 -419
  434. /nautobot/dcim/templates/dcim/{platform_edit.html → platform_create.html} +0 -0
  435. /nautobot/extras/test_jobs/{pass.py → pass_job.py} +0 -0
  436. {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/LICENSE.txt +0 -0
  437. {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/NOTICE +0 -0
  438. {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/WHEEL +0 -0
  439. {nautobot-2.4.10.dist-info → nautobot-2.4.12.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  import contextlib
2
2
  import re
3
3
  from typing import Optional, Sequence
4
- from unittest import skipIf
4
+ from unittest import mock, skipIf
5
5
  import uuid
6
6
 
7
7
  from django.apps import apps
@@ -9,6 +9,7 @@ from django.conf import settings
9
9
  from django.contrib.contenttypes.models import ContentType
10
10
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
11
11
  from django.core.validators import URLValidator
12
+ from django.db.models import ManyToManyField, Model, QuerySet
12
13
  from django.test import override_settings, tag, TestCase as _TestCase
13
14
  from django.urls import NoReverseMatch, reverse
14
15
  from django.utils.html import escape
@@ -16,6 +17,7 @@ from django.utils.http import urlencode
16
17
  from django.utils.text import slugify
17
18
  from tree_queries.models import TreeNode
18
19
 
20
+ from nautobot.core.jobs.bulk_actions import BulkEditObjects
19
21
  from nautobot.core.models.generics import PrimaryModel
20
22
  from nautobot.core.models.tree_queries import TreeModel
21
23
  from nautobot.core.templatetags import helpers
@@ -141,6 +143,8 @@ class ViewTestCases:
141
143
  Retrieve a single instance.
142
144
  """
143
145
 
146
+ custom_action_required_permissions = {}
147
+
144
148
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
145
149
  def test_get_object_anonymous(self):
146
150
  # Make the request as an unauthenticated user
@@ -246,6 +250,21 @@ class ViewTestCases:
246
250
  self.assertBodyContains(response, f"{instance.get_absolute_url()}#advanced")
247
251
  self.assertBodyContains(response, "Advanced")
248
252
 
253
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
254
+ def test_custom_actions(self):
255
+ instance = self._get_queryset().first()
256
+ for url_name, required_permissions in self.custom_action_required_permissions.items():
257
+ url = reverse(url_name, kwargs={"pk": instance.pk})
258
+ self.assertHttpStatus(self.client.get(url), 403)
259
+ for permission in required_permissions[:-1]:
260
+ self.add_permissions(permission)
261
+ self.assertHttpStatus(self.client.get(url), 403)
262
+
263
+ self.add_permissions(required_permissions[-1])
264
+ self.assertHttpStatus(self.client.get(url), 200)
265
+ # delete the permissions here so that repetitive calls to add_permissions do not create duplicate permissions.
266
+ self.remove_permissions(*required_permissions)
267
+
249
268
  class GetObjectChangelogViewTestCase(ModelViewTestCase):
250
269
  """
251
270
  View the changelog for an instance.
@@ -769,6 +788,17 @@ class ViewTestCases:
769
788
  response_body = response.content.decode(response.charset)
770
789
  self.assertIn("/login/?next=" + self._get_url("list"), response_body, msg=response_body)
771
790
 
791
+ def test_list_objects_anonymous_with_exempt_permission_for_one_view_only(self):
792
+ # Make the request as an unauthenticated user
793
+ self.client.logout()
794
+ # Test if AnonymousUser can properly render the whole list view
795
+ with override_settings(EXEMPT_VIEW_PERMISSIONS=[self.model._meta.label_lower]):
796
+ response = self.client.get(self._get_url("list"))
797
+ self.assertHttpStatus(response, 200)
798
+ # There should be some rows
799
+ self.assertBodyContains(response, '<tr class="even')
800
+ self.assertBodyContains(response, '<tr class="odd')
801
+
772
802
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
773
803
  def test_list_objects_filtered(self):
774
804
  instance1, instance2 = self._get_queryset().all()[:2]
@@ -1049,6 +1079,7 @@ class ViewTestCases:
1049
1079
  def validate_redirect_to_job_result(self, response):
1050
1080
  # Get the last Bulk Edit Objects JobResult created
1051
1081
  job_result = JobResult.objects.filter(name="Bulk Edit Objects").first()
1082
+ self.assertIsNotNone(job_result, "No JobResult was created - likely the bulk_edit_data is invalid!")
1052
1083
  # Assert redirect to Job Results
1053
1084
  self.assertRedirects(
1054
1085
  response,
@@ -1082,8 +1113,71 @@ class ViewTestCases:
1082
1113
  # Assign model-level permission
1083
1114
  self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
1084
1115
 
1085
- response = self.client.post(self._get_url("bulk_edit"), data)
1086
- self.validate_redirect_to_job_result(response)
1116
+ with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
1117
+ response = self.client.post(self._get_url("bulk_edit"), data)
1118
+ self.validate_redirect_to_job_result(response)
1119
+ mock_enqueue_job.assert_called()
1120
+
1121
+ # Verify that the provided self.bulk_edit_data was passed through correctly to the job.
1122
+ # The below is a bit gross because of multiple layers of data encoding and decoding involved. Sorry!
1123
+ job_form = BulkEditObjects.as_form(BulkEditObjects.deserialize_data(mock_enqueue_job.call_args.kwargs))
1124
+ job_form.is_valid()
1125
+ job_kwargs = job_form.cleaned_data
1126
+
1127
+ bulk_edit_form_class = lookup.get_form_for_model(self.model, form_prefix="BulkEdit")
1128
+ bulk_edit_form = bulk_edit_form_class(self.model, job_kwargs["form_data"])
1129
+ bulk_edit_form.is_valid()
1130
+ passed_bulk_edit_data = bulk_edit_form.cleaned_data
1131
+
1132
+ for key, value in self.bulk_edit_data.items():
1133
+ with self.subTest(key=key):
1134
+ if isinstance(passed_bulk_edit_data.get(key), Model):
1135
+ self.assertEqual(passed_bulk_edit_data.get(key).pk, value)
1136
+ elif isinstance(passed_bulk_edit_data.get(key), QuerySet):
1137
+ self.assertEqual(
1138
+ sorted(passed_bulk_edit_data.get(key).values_list("pk", flat=True)), sorted(value)
1139
+ )
1140
+ else:
1141
+ self.assertEqual(passed_bulk_edit_data.get(key), bulk_edit_form.fields[key].clean(value))
1142
+
1143
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1144
+ def test_bulk_edit_objects_nullable_fields(self):
1145
+ """Assert that "set null" fields on the bulk-edit form are correctly passed through to the job."""
1146
+ bulk_edit_form_class = lookup.get_form_for_model(self.model, form_prefix="BulkEdit")
1147
+ bulk_edit_form = bulk_edit_form_class(self.model)
1148
+ if not getattr(bulk_edit_form, "nullable_fields", ()):
1149
+ self.skipTest(f"no nullable fields on {bulk_edit_form_class}")
1150
+
1151
+ for field_name in bulk_edit_form.nullable_fields:
1152
+ with self.subTest(field_name=field_name):
1153
+ if field_name.startswith("cf_"):
1154
+ # TODO check whether customfield is nullable
1155
+ continue
1156
+ if field_name.startswith("cr_"):
1157
+ # TODO check whether relationship is required
1158
+ continue
1159
+ model_field = self.model._meta.get_field(field_name)
1160
+ if isinstance(model_field, ManyToManyField):
1161
+ # always nullable
1162
+ continue
1163
+ self.assertTrue(model_field.null or model_field.blank)
1164
+
1165
+ pk_list = list(self._get_queryset().values_list("pk", flat=True)[:3])
1166
+ data = {
1167
+ "pk": pk_list,
1168
+ "_apply": True, # Form button
1169
+ "_nullify": list(bulk_edit_form.nullable_fields),
1170
+ }
1171
+
1172
+ # Assign model-level permission
1173
+ self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
1174
+
1175
+ with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
1176
+ response = self.client.post(self._get_url("bulk_edit"), data)
1177
+ self.validate_redirect_to_job_result(response)
1178
+ mock_enqueue_job.assert_called()
1179
+
1180
+ self.assertEqual(mock_enqueue_job.call_args.kwargs["form_data"].get("_nullify"), data["_nullify"])
1087
1181
 
1088
1182
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
1089
1183
  def test_bulk_edit_form_contains_all_pks(self):
@@ -1182,11 +1276,13 @@ class ViewTestCases:
1182
1276
  }
1183
1277
  data.update(utils.post_data(self.bulk_edit_data))
1184
1278
 
1185
- # Attempt to bulk edit permitted objects into a non-permitted state
1186
- response = self.client.post(self._get_url("bulk_edit"), data)
1187
- # NOTE: There is no way of testing constrained failure as bulk edit is a system Job;
1188
- # and can only be tested by checking JobLogs.
1189
- self.validate_redirect_to_job_result(response)
1279
+ with mock.patch.object(JobResult, "enqueue_job", wraps=JobResult.enqueue_job) as mock_enqueue_job:
1280
+ # Attempt to bulk edit permitted objects into a non-permitted state
1281
+ response = self.client.post(self._get_url("bulk_edit"), data)
1282
+ # NOTE: There is no way of testing constrained failure as bulk edit is a system Job;
1283
+ # and can only be tested by checking JobLogs.
1284
+ self.validate_redirect_to_job_result(response)
1285
+ mock_enqueue_job.assert_called()
1190
1286
 
1191
1287
  class BulkDeleteObjectsViewTestCase(ModelViewTestCase):
1192
1288
  """
@@ -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.
@@ -17,6 +17,7 @@ from nautobot.core.models import fields as core_fields, utils as models_utils, v
17
17
  from nautobot.core.testing import TestCase
18
18
  from nautobot.core.utils import data as data_utils, filtering, lookup, querysets, requests
19
19
  from nautobot.core.utils.migrations import update_object_change_ct_for_replaced_models
20
+ from nautobot.core.utils.module_loading import check_name_safe_to_import_privately
20
21
  from nautobot.dcim import filters as dcim_filters, forms as dcim_forms, models as dcim_models, tables
21
22
  from nautobot.extras import models as extras_models, utils as extras_utils
22
23
  from nautobot.extras.choices import ObjectChangeActionChoices, RelationshipTypeChoices
@@ -959,6 +960,22 @@ class TestMigrationUtils(TestCase):
959
960
  self.assertEqual(ObjectChange.objects.get(request_id=request_id).related_object_type, location_ct)
960
961
 
961
962
 
963
+ class TestModuleLoadingUtils(TestCase):
964
+ def test_check_name_safe_to_import_privately(self):
965
+ for invalid in (
966
+ "foo.bar", # not a valid identifier
967
+ "😂", # not a valid identifier
968
+ "from", # reserved keyword
969
+ "sys", # Python builtin
970
+ "nautobot", # installed package
971
+ "tkinter", # system library
972
+ ):
973
+ with self.subTest(f"Invalid name: {invalid}"):
974
+ permitted, reason = check_name_safe_to_import_privately(invalid)
975
+ self.assertFalse(permitted)
976
+ self.assertIsInstance(reason, str)
977
+
978
+
962
979
  class TestQuerySetUtils(TestCase):
963
980
  def test_maybe_select_related(self):
964
981
  # If possible, select_related should be called
@@ -764,11 +764,11 @@ class TestObjectDetailView(TestCase):
764
764
  url = reverse("circuits:provider", args=(provider.pk,))
765
765
  response = self.client.get(f"{url}?tab=main")
766
766
  self.assertHttpStatus(response, 200)
767
- response_data = response.content.decode(response.charset)
767
+ response_data = extract_page_body(response.content.decode(response.charset))
768
768
  view_move_url = reverse("circuits:circuit_list") + f"?provider={provider.id}"
769
769
 
770
770
  # Assert Badge Count in table panel header
771
- panel_header = f"""<div class="panel-heading"><strong>Circuits</strong> <a href="{view_move_url}" class="badge badge-primary">10</a></div>"""
771
+ panel_header = f"""<strong>Circuits</strong> <a href="{view_move_url}" class="badge badge-primary">10</a>"""
772
772
  self.assertInHTML(panel_header, response_data)
773
773
 
774
774
  # Assert view X more btn
@@ -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)
@@ -867,7 +867,7 @@ class ObjectsTablePanel(Panel):
867
867
  body_content_table_queryset = body_content_table_queryset.order_by(*self.order_by_fields)
868
868
  body_content_table_queryset = body_content_table_queryset.distinct()
869
869
  body_content_table = body_content_table_class(
870
- body_content_table_queryset, hide_hierarchy_ui=self.hide_hierarchy_ui
870
+ body_content_table_queryset, hide_hierarchy_ui=self.hide_hierarchy_ui, user=request.user
871
871
  )
872
872
  if self.tab_id and "actions" in body_content_table.columns:
873
873
  # Use the `self.tab_id`, if it exists, to determine the correct return URL for the table
@@ -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):
@@ -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
 
@@ -1,6 +1,7 @@
1
1
  from contextlib import contextmanager
2
2
  import importlib
3
3
  from importlib.util import find_spec, module_from_spec
4
+ from keyword import iskeyword
4
5
  import logging
5
6
  import os
6
7
  import pkgutil
@@ -33,6 +34,28 @@ def clear_module_from_sys_modules(module_name):
33
34
  del sys.modules[name]
34
35
 
35
36
 
37
+ def check_name_safe_to_import_privately(name: str) -> tuple[bool, str]:
38
+ """
39
+ Make sure the given package/module name is "safe" to import from the filesystem.
40
+
41
+ In other words, make sure it's:
42
+ - a valid Python identifier and not a reserved keyword
43
+ - not the name of an existing "real" Python package or builtin
44
+
45
+ Returns:
46
+ (bool, str): Whether safe to load, and an explanatory string fragment for logging/exception messages.
47
+ """
48
+ if not name.isidentifier():
49
+ return False, "not a valid identifier"
50
+ if iskeyword(name):
51
+ return False, "a reserved keyword"
52
+ if name in sys.builtin_module_names:
53
+ return False, "a Python builtin"
54
+ if any(module_info.name == name for module_info in pkgutil.iter_modules()):
55
+ return False, "the name of an installed Python package"
56
+ return True, "a valid and non-conflicting module name"
57
+
58
+
36
59
  def import_modules_privately(path, module_path=None, ignore_import_errors=True):
37
60
  """
38
61
  Import modules from the filesystem without adding the path permanently to `sys.path`.
@@ -54,6 +54,7 @@ from nautobot.core.views.utils import (
54
54
  check_filter_for_display,
55
55
  common_detail_view_context,
56
56
  get_csv_form_fields_from_serializer_class,
57
+ get_saved_views_for_user,
57
58
  handle_protectederror,
58
59
  import_csv_helper,
59
60
  prepare_cloned_fields,
@@ -315,18 +316,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
315
316
  table_config_form = None
316
317
  current_saved_view = None
317
318
  current_saved_view_pk = self.request.GET.get("saved_view", None)
318
- # We are not using .restrict(request.user, "view") here
319
- # User should be able to see any saved view that he has the list view access to.
320
- if user.has_perms(["extras.view_savedview"]):
321
- saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
322
- else:
323
- shared_saved_views = (
324
- SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
325
- )
326
- user_owned_saved_views = (
327
- SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
328
- )
329
- saved_views = shared_saved_views | user_owned_saved_views
319
+ saved_views = get_saved_views_for_user(user, list_url)
330
320
 
331
321
  if current_saved_view_pk:
332
322
  try:
@@ -239,6 +239,9 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
239
239
  table_class = None
240
240
  notes_form_class = NoteForm
241
241
  permission_classes = []
242
+ # custom view attributes used for permission checks and handling
243
+ custom_view_base_action = None
244
+ custom_view_additional_permissions = None
242
245
 
243
246
  def get_permissions_for_model(self, model, actions):
244
247
  """
@@ -249,6 +252,10 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
249
252
  """
250
253
  model_permissions = []
251
254
  for action in actions:
255
+ # Append additional object permissions if specified.
256
+ if self.custom_view_additional_permissions:
257
+ model_permissions.append(*self.custom_view_additional_permissions)
258
+ # Append the model-level permissions for the action.
252
259
  model_permissions.append(f"{model._meta.app_label}.{action}_{model._meta.model_name}")
253
260
  return model_permissions
254
261
 
@@ -501,7 +508,13 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
501
508
 
502
509
  def get_action(self):
503
510
  """Helper method for retrieving action and if action not set defaulting to action name."""
504
- return PERMISSIONS_ACTION_MAP.get(self.action, self.action)
511
+ if self.custom_view_base_action:
512
+ return self.custom_view_base_action
513
+ if self.action in PERMISSIONS_ACTION_MAP:
514
+ # If the action is in the action_map, return the mapped permission
515
+ return PERMISSIONS_ACTION_MAP[self.action]
516
+
517
+ return self.action
505
518
 
506
519
  def get_extra_context(self, request, instance=None):
507
520
  """
@@ -1032,6 +1045,11 @@ class BulkEditAndBulkDeleteModelMixin:
1032
1045
 
1033
1046
  filter_query_params = new_filter_query_params
1034
1047
 
1048
+ if nullified_fields := request.POST.getlist("_nullify"):
1049
+ form_data["_nullify"] = nullified_fields
1050
+ else:
1051
+ form_data["_nullify"] = []
1052
+
1035
1053
  job_form = BulkEditObjects.as_form(
1036
1054
  data={
1037
1055
  "form_data": form_data,
@@ -28,6 +28,7 @@ from nautobot.core.views.utils import (
28
28
  check_filter_for_display,
29
29
  common_detail_view_context,
30
30
  get_csv_form_fields_from_serializer_class,
31
+ get_saved_views_for_user,
31
32
  view_changes_not_saved,
32
33
  )
33
34
  from nautobot.extras.models import SavedView
@@ -235,7 +236,8 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
235
236
  if view.filterset_form_class is not None:
236
237
  filter_form = view.filterset_form_class(view.filter_params, label_suffix="")
237
238
  table = self.construct_table(view, request=request, permissions=permissions)
238
- search_form = SearchForm(data=view.filter_params)
239
+ q_placeholder = "Search " + bettertitle(model._meta.verbose_name_plural)
240
+ search_form = SearchForm(data=view.filter_params, q_placeholder=q_placeholder)
239
241
  elif view.action == "destroy":
240
242
  form = form_class(initial=request.GET)
241
243
  elif view.action in ["create", "update"]:
@@ -309,18 +311,7 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
309
311
  list_url = f"{resolved_path.app_name}:{resolved_path.url_name}"
310
312
  saved_views = None
311
313
  if model.is_saved_view_model:
312
- # We are not using .restrict(request.user, "view") here
313
- # User should be able to see any saved view that he has the list view access to.
314
- if request.user.has_perms(["extras.view_savedview"]):
315
- saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
316
- else:
317
- shared_saved_views = (
318
- SavedView.objects.filter(view=list_url, is_shared=True).order_by("name").only("pk", "name")
319
- )
320
- user_owned_saved_views = (
321
- SavedView.objects.filter(view=list_url, owner=request.user).order_by("name").only("pk", "name")
322
- )
323
- saved_views = shared_saved_views | user_owned_saved_views
314
+ saved_views = get_saved_views_for_user(request.user, list_url)
324
315
 
325
316
  new_changes_not_applied = view_changes_not_saved(request, view, self.saved_view)
326
317
  context.update(
@@ -17,6 +17,7 @@ from nautobot.core.utils.data import is_uuid
17
17
  from nautobot.core.utils.filtering import get_filter_field_label
18
18
  from nautobot.core.utils.lookup import get_created_and_last_updated_usernames_for_model, get_form_for_model
19
19
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
20
+ from nautobot.extras.models import SavedView
20
21
  from nautobot.extras.tables import AssociatedContactsTable, DynamicGroupTable, ObjectMetadataTable
21
22
 
22
23
 
@@ -382,3 +383,18 @@ def common_detail_view_context(request, instance):
382
383
  context["associated_object_metadata_table"] = None
383
384
 
384
385
  return context
386
+
387
+
388
+ def get_saved_views_for_user(user, list_url):
389
+ # We are not using .restrict(request.user, "view") here
390
+ # User should be able to see any saved view that he has the list view access to.
391
+ saved_views = SavedView.objects.filter(view=list_url).order_by("name").only("pk", "name")
392
+ if user.has_perms(["extras.view_savedview"]):
393
+ return saved_views
394
+
395
+ shared_saved_views = saved_views.filter(is_shared=True)
396
+ if user.is_authenticated:
397
+ user_owned_saved_views = SavedView.objects.filter(view=list_url, owner=user).order_by("name").only("pk", "name")
398
+ return shared_saved_views | user_owned_saved_views
399
+
400
+ return shared_saved_views
@@ -79,6 +79,7 @@ from nautobot.dcim.models import (
79
79
  Module,
80
80
  ModuleBay,
81
81
  ModuleBayTemplate,
82
+ ModuleFamily,
82
83
  ModuleType,
83
84
  PathEndpoint,
84
85
  Platform,
@@ -1124,3 +1125,15 @@ class InterfaceVDCAssignmentSerializer(ValidatedModelSerializer):
1124
1125
  class Meta:
1125
1126
  model = InterfaceVDCAssignment
1126
1127
  fields = "__all__"
1128
+
1129
+
1130
+ class ModuleFamilySerializer(NautobotModelSerializer):
1131
+ """API serializer for ModuleFamily objects."""
1132
+
1133
+ url = serializers.HyperlinkedIdentityField(view_name="dcim-api:modulefamily-detail")
1134
+ module_type_count = serializers.IntegerField(read_only=True)
1135
+ module_bay_count = serializers.IntegerField(read_only=True)
1136
+
1137
+ class Meta:
1138
+ model = ModuleFamily
1139
+ fields = "__all__"
nautobot/dcim/api/urls.py CHANGED
@@ -17,6 +17,7 @@ router.register("rack-reservations", views.RackReservationViewSet)
17
17
  router.register("manufacturers", views.ManufacturerViewSet)
18
18
  router.register("device-families", views.DeviceFamilyViewSet)
19
19
  router.register("device-types", views.DeviceTypeViewSet)
20
+ router.register("module-families", views.ModuleFamilyViewSet)
20
21
  router.register("module-types", views.ModuleTypeViewSet)
21
22
 
22
23
  # Device type and Module type components
@@ -54,6 +54,7 @@ from nautobot.dcim.models import (
54
54
  Module,
55
55
  ModuleBay,
56
56
  ModuleBayTemplate,
57
+ ModuleFamily,
57
58
  ModuleType,
58
59
  Platform,
59
60
  PowerFeed,
@@ -644,6 +645,14 @@ class CableViewSet(NautobotModelViewSet):
644
645
  serializer_class = serializers.CableSerializer
645
646
  filterset_class = filters.CableFilterSet
646
647
 
648
+ def get_queryset(self):
649
+ # 6933 fix: with prefetch related in queryset
650
+ # DeviceInterface is not properly cleared of _path_id
651
+ queryset = super().get_queryset()
652
+ if self.action == "destroy":
653
+ queryset = queryset.prefetch_related(None)
654
+ return queryset
655
+
647
656
 
648
657
  #
649
658
  # Virtual chassis
@@ -836,3 +845,14 @@ class InterfaceVDCAssignmentViewSet(ModelViewSet):
836
845
  queryset = InterfaceVDCAssignment.objects.all()
837
846
  serializer_class = serializers.InterfaceVDCAssignmentSerializer
838
847
  filterset_class = filters.InterfaceVDCAssignmentFilterSet
848
+
849
+
850
+ class ModuleFamilyViewSet(NautobotModelViewSet):
851
+ """API viewset for interacting with ModuleFamily objects."""
852
+
853
+ queryset = ModuleFamily.objects.annotate(
854
+ module_type_count=count_related(ModuleType, "module_family"),
855
+ module_bay_count=count_related(ModuleBay, "module_family"),
856
+ )
857
+ serializer_class = serializers.ModuleFamilySerializer
858
+ filterset_class = filters.ModuleFamilyFilterSet
nautobot/dcim/apps.py CHANGED
@@ -14,6 +14,7 @@ class DCIMConfig(NautobotConfig):
14
14
  "devicetype",
15
15
  "location",
16
16
  "module",
17
+ "modulefamily",
17
18
  "moduletype",
18
19
  "powerfeed",
19
20
  "rack",
nautobot/dcim/factory.py CHANGED
@@ -49,6 +49,7 @@ from nautobot.dcim.models import (
49
49
  Module,
50
50
  ModuleBay,
51
51
  ModuleBayTemplate,
52
+ ModuleFamily,
52
53
  ModuleType,
53
54
  Platform,
54
55
  PowerOutletTemplate,
@@ -787,6 +788,7 @@ class ModuleTypeFactory(PrimaryModelFactory):
787
788
  exclude = ("has_part_number", "has_comments")
788
789
 
789
790
  manufacturer = random_instance(Manufacturer, allow_null=False)
791
+ module_family = random_instance(ModuleFamily, allow_null=True)
790
792
 
791
793
  has_part_number = NautobotBoolIterator()
792
794
  part_number = factory.Maybe("has_part_number", factory.Faker("ean", length=8), "")
@@ -970,6 +972,15 @@ class ModuleBayTemplateFactory(ModularDeviceComponentTemplateFactory):
970
972
  factory.LazyAttribute(lambda o: o.module_type.module_bay_templates.count() + 1),
971
973
  )
972
974
 
975
+ class Params:
976
+ has_module_family = NautobotBoolIterator()
977
+
978
+ module_family = factory.Maybe(
979
+ "has_module_family",
980
+ random_instance(ModuleFamily),
981
+ None,
982
+ )
983
+
973
984
 
974
985
  class VirtualDeviceContextFactory(PrimaryModelFactory):
975
986
  class Meta: