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
nautobot/dcim/views.py CHANGED
@@ -16,6 +16,7 @@ from django.forms import (
16
16
  MultipleHiddenInput,
17
17
  )
18
18
  from django.shortcuts import get_object_or_404, HttpResponse, redirect, render
19
+ from django.template.loader import render_to_string
19
20
  from django.urls import reverse
20
21
  from django.utils.encoding import iri_to_uri
21
22
  from django.utils.functional import cached_property
@@ -69,8 +70,9 @@ from nautobot.wireless.models import (
69
70
  ControllerManagedDeviceGroupWirelessNetworkAssignment,
70
71
  )
71
72
  from nautobot.wireless.tables import (
73
+ BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
72
74
  ControllerManagedDeviceGroupRadioProfileAssignmentTable,
73
- ControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
75
+ DeviceGroupWirelessNetworkTable,
74
76
  RadioProfileTable,
75
77
  )
76
78
 
@@ -106,6 +108,7 @@ from .models import (
106
108
  Module,
107
109
  ModuleBay,
108
110
  ModuleBayTemplate,
111
+ ModuleFamily,
109
112
  ModuleType,
110
113
  PathEndpoint,
111
114
  Platform,
@@ -251,21 +254,22 @@ class LocationTypeUIViewSet(NautobotUIViewSet):
251
254
  #
252
255
 
253
256
 
254
- class LocationListView(generic.ObjectListView):
255
- queryset = Location.objects.all()
256
- filterset = filters.LocationFilterSet
257
- filterset_form = forms.LocationFilterForm
258
- table = tables.LocationTable
259
-
260
-
261
- class LocationView(generic.ObjectView):
257
+ class LocationUIViewSet(NautobotUIViewSet):
262
258
  # We aren't accessing tree fields anywhere so this is safe (note that `parent` itself is a normal foreign
263
259
  # key, not a tree field). If we ever do access tree fields, this will perform worse, because django will
264
260
  # automatically issue a second query (similar to behavior for
265
261
  # https://docs.djangoproject.com/en/3.2/ref/models/querysets/#django.db.models.query.QuerySet.only)
266
- queryset = Location.objects.without_tree_fields().all()
262
+ queryset = Location.objects.without_tree_fields().select_related("location_type", "parent", "tenant")
263
+ filterset_class = filters.LocationFilterSet
264
+ filterset_form_class = forms.LocationFilterForm
265
+ table_class = tables.LocationTable
266
+ form_class = forms.LocationForm
267
+ bulk_update_form_class = forms.LocationBulkEditForm
268
+ serializer_class = serializers.LocationSerializer
267
269
 
268
270
  def get_extra_context(self, request, instance):
271
+ if instance is None:
272
+ return super().get_extra_context(request, instance)
269
273
  related_locations = (
270
274
  instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
271
275
  )
@@ -305,7 +309,6 @@ class LocationView(generic.ObjectView):
305
309
  )
306
310
 
307
311
  children_table = tables.LocationTable(children, hide_hierarchy_ui=True)
308
-
309
312
  paginate = {
310
313
  "paginator_class": EnhancedPaginator,
311
314
  "per_page": get_paginate_count(request),
@@ -323,34 +326,6 @@ class LocationView(generic.ObjectView):
323
326
  }
324
327
 
325
328
 
326
- class LocationEditView(generic.ObjectEditView):
327
- queryset = Location.objects.all()
328
- model_form = forms.LocationForm
329
- template_name = "dcim/location_edit.html"
330
-
331
-
332
- class LocationDeleteView(generic.ObjectDeleteView):
333
- queryset = Location.objects.all()
334
-
335
-
336
- class LocationBulkEditView(generic.BulkEditView):
337
- queryset = Location.objects.select_related("location_type", "parent", "tenant")
338
- filterset = filters.LocationFilterSet
339
- table = tables.LocationTable
340
- form = forms.LocationBulkEditForm
341
-
342
-
343
- class LocationBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
344
- queryset = Location.objects.all()
345
- table = tables.LocationTable
346
-
347
-
348
- class LocationBulkDeleteView(generic.BulkDeleteView):
349
- queryset = Location.objects.select_related("location_type", "parent", "tenant")
350
- filterset = filters.LocationFilterSet
351
- table = tables.LocationTable
352
-
353
-
354
329
  class MigrateLocationDataToContactView(generic.ObjectEditView):
355
330
  queryset = Location.objects.all()
356
331
  model_form = LocationMigrateDataToContactForm
@@ -1623,7 +1598,14 @@ class ModuleBayTemplateUIViewSet(
1623
1598
  return parent.display
1624
1599
  return ""
1625
1600
 
1626
- @action(detail=False, methods=["GET", "POST"], url_path="rename", url_name="bulk_rename")
1601
+ @action(
1602
+ detail=False,
1603
+ methods=["GET", "POST"],
1604
+ url_path="rename",
1605
+ url_name="bulk_rename",
1606
+ custom_view_base_action="change",
1607
+ custom_view_additional_permissions=["dcim.change_modulebaytemplate"],
1608
+ )
1627
1609
  def bulk_rename(self, request, *args, **kwargs):
1628
1610
  return self._bulk_rename(request, *args, **kwargs)
1629
1611
 
@@ -2256,7 +2238,7 @@ class DeviceWirelessView(generic.ObjectView):
2256
2238
  wireless_networks = ControllerManagedDeviceGroupWirelessNetworkAssignment.objects.filter(
2257
2239
  controller_managed_device_group=controller_managed_device_group
2258
2240
  ).select_related("wireless_network", "controller_managed_device_group", "vlan")
2259
- wireless_networks_table = ControllerManagedDeviceGroupWirelessNetworkAssignmentTable(
2241
+ wireless_networks_table = BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable(
2260
2242
  data=wireless_networks, user=request.user, orderable=False
2261
2243
  )
2262
2244
  wireless_networks_table.columns.hide("controller_managed_device_group")
@@ -2393,35 +2375,6 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2393
2375
  table_class = tables.ModuleTable
2394
2376
  component_model = None
2395
2377
 
2396
- def get_action(self):
2397
- if self.component_model:
2398
- method = self.request.method.lower()
2399
- if method == "get":
2400
- return "view"
2401
- else:
2402
- return "change"
2403
-
2404
- return super().get_action()
2405
-
2406
- def get_required_permission(self):
2407
- # TODO: standardize a pattern for permissions enforcement on custom actions
2408
- if self.component_model:
2409
- model = self.component_model
2410
- method = self.request.method.lower()
2411
- if method == "get":
2412
- component_action = "view"
2413
- permissions = [*self.get_permissions_for_model(model, [component_action]), "dcim.view_module"]
2414
- elif self.action.startswith("bulk_add"):
2415
- component_action = "add"
2416
- permissions = [*self.get_permissions_for_model(model, [component_action]), "dcim.change_module"]
2417
- else:
2418
- component_action = "change"
2419
- permissions = [*self.get_permissions_for_model(model, [component_action]), "dcim.change_module"]
2420
-
2421
- return permissions
2422
-
2423
- return super().get_required_permission()
2424
-
2425
2378
  def get_extra_context(self, request, instance):
2426
2379
  context = super().get_extra_context(request, instance)
2427
2380
  if instance:
@@ -2448,7 +2401,13 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2448
2401
 
2449
2402
  return active_parent_tab
2450
2403
 
2451
- @action(detail=True, url_path="console-ports", component_model=ConsolePort)
2404
+ @action(
2405
+ detail=True,
2406
+ url_path="console-ports",
2407
+ component_model=ConsolePort,
2408
+ custom_view_base_action="view",
2409
+ custom_view_additional_permissions=["dcim.view_consoleport"],
2410
+ )
2452
2411
  def consoleports(self, request, *args, **kwargs):
2453
2412
  instance = self.get_object()
2454
2413
  consoleports = (
@@ -2467,7 +2426,13 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2467
2426
  }
2468
2427
  )
2469
2428
 
2470
- @action(detail=True, url_path="console-server-ports", component_model=ConsoleServerPort)
2429
+ @action(
2430
+ detail=True,
2431
+ url_path="console-server-ports",
2432
+ component_model=ConsoleServerPort,
2433
+ custom_view_base_action="view",
2434
+ custom_view_additional_permissions=["dcim.view_consoleserverport"],
2435
+ )
2471
2436
  def consoleserverports(self, request, *args, **kwargs):
2472
2437
  instance = self.get_object()
2473
2438
  consoleserverports = (
@@ -2490,7 +2455,13 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2490
2455
  }
2491
2456
  )
2492
2457
 
2493
- @action(detail=True, url_path="power-ports", component_model=PowerPort)
2458
+ @action(
2459
+ detail=True,
2460
+ url_path="power-ports",
2461
+ component_model=PowerPort,
2462
+ custom_view_base_action="view",
2463
+ custom_view_additional_permissions=["dcim.view_powerport"],
2464
+ )
2494
2465
  def powerports(self, request, *args, **kwargs):
2495
2466
  instance = self.get_object()
2496
2467
  powerports = (
@@ -2511,7 +2482,13 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2511
2482
  }
2512
2483
  )
2513
2484
 
2514
- @action(detail=True, url_path="power-outlets", component_model=PowerOutlet)
2485
+ @action(
2486
+ detail=True,
2487
+ url_path="power-outlets",
2488
+ component_model=PowerOutlet,
2489
+ custom_view_base_action="view",
2490
+ custom_view_additional_permissions=["dcim.view_poweroutlet"],
2491
+ )
2515
2492
  def poweroutlets(self, request, *args, **kwargs):
2516
2493
  instance = self.get_object()
2517
2494
  poweroutlets = (
@@ -2532,7 +2509,12 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2532
2509
  }
2533
2510
  )
2534
2511
 
2535
- @action(detail=True, component_model=Interface)
2512
+ @action(
2513
+ detail=True,
2514
+ component_model=Interface,
2515
+ custom_view_base_action="view",
2516
+ custom_view_additional_permissions=["dcim.view_interface"],
2517
+ )
2536
2518
  def interfaces(self, request, *args, **kwargs):
2537
2519
  instance = self.get_object()
2538
2520
  interfaces = (
@@ -2558,7 +2540,13 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2558
2540
  }
2559
2541
  )
2560
2542
 
2561
- @action(detail=True, url_path="front-ports", component_model=FrontPort)
2543
+ @action(
2544
+ detail=True,
2545
+ url_path="front-ports",
2546
+ component_model=FrontPort,
2547
+ custom_view_base_action="view",
2548
+ custom_view_additional_permissions=["dcim.view_frontport"],
2549
+ )
2562
2550
  def frontports(self, request, *args, **kwargs):
2563
2551
  instance = self.get_object()
2564
2552
  frontports = instance.front_ports.restrict(request.user, "view").select_related("cable", "rear_port")
@@ -2575,7 +2563,13 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2575
2563
  },
2576
2564
  )
2577
2565
 
2578
- @action(detail=True, url_path="rear-ports", component_model=RearPort)
2566
+ @action(
2567
+ detail=True,
2568
+ url_path="rear-ports",
2569
+ component_model=RearPort,
2570
+ custom_view_base_action="view",
2571
+ custom_view_additional_permissions=["dcim.view_rearport"],
2572
+ )
2579
2573
  def rearports(self, request, *args, **kwargs):
2580
2574
  instance = self.get_object()
2581
2575
  rearports = instance.rear_ports.restrict(request.user, "view").select_related("cable")
@@ -2592,7 +2586,13 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2592
2586
  }
2593
2587
  )
2594
2588
 
2595
- @action(detail=True, url_path="module-bays", component_model=ModuleBay)
2589
+ @action(
2590
+ detail=True,
2591
+ url_path="module-bays",
2592
+ component_model=ModuleBay,
2593
+ custom_view_base_action="view",
2594
+ custom_view_additional_permissions=["dcim.view_modulebay"],
2595
+ )
2596
2596
  def modulebays(self, request, *args, **kwargs):
2597
2597
  instance = self.get_object()
2598
2598
  modulebays = instance.module_bays.restrict(request.user, "view").prefetch_related(
@@ -2615,6 +2615,8 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2615
2615
  url_path="console-ports/add",
2616
2616
  url_name="bulk_add_consoleport",
2617
2617
  component_model=ConsolePort,
2618
+ custom_view_base_action="change",
2619
+ custom_view_additional_permissions=["dcim.add_consoleport"],
2618
2620
  )
2619
2621
  def bulk_add_consoleport(self, request, *args, **kwargs):
2620
2622
  return self._bulk_component_create(
@@ -2629,6 +2631,8 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2629
2631
  url_path="console-server-ports/add",
2630
2632
  url_name="bulk_add_consoleserverport",
2631
2633
  component_model=ConsoleServerPort,
2634
+ custom_view_base_action="change",
2635
+ custom_view_additional_permissions=["dcim.add_consoleserverport"],
2632
2636
  )
2633
2637
  def bulk_add_consoleserverport(self, request, *args, **kwargs):
2634
2638
  return self._bulk_component_create(
@@ -2643,6 +2647,8 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2643
2647
  url_path="power-ports/add",
2644
2648
  url_name="bulk_add_powerport",
2645
2649
  component_model=PowerPort,
2650
+ custom_view_base_action="change",
2651
+ custom_view_additional_permissions=["dcim.add_powerport"],
2646
2652
  )
2647
2653
  def bulk_add_powerport(self, request, *args, **kwargs):
2648
2654
  return self._bulk_component_create(
@@ -2657,6 +2663,8 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2657
2663
  url_path="power-outlets/add",
2658
2664
  url_name="bulk_add_poweroutlet",
2659
2665
  component_model=PowerOutlet,
2666
+ custom_view_base_action="change",
2667
+ custom_view_additional_permissions=["dcim.add_poweroutlet"],
2660
2668
  )
2661
2669
  def bulk_add_poweroutlet(self, request, *args, **kwargs):
2662
2670
  return self._bulk_component_create(
@@ -2671,6 +2679,8 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2671
2679
  url_path="interfaces/add",
2672
2680
  url_name="bulk_add_interface",
2673
2681
  component_model=Interface,
2682
+ custom_view_base_action="change",
2683
+ custom_view_additional_permissions=["dcim.add_interface"],
2674
2684
  )
2675
2685
  def bulk_add_interface(self, request, *args, **kwargs):
2676
2686
  return self._bulk_component_create(
@@ -2685,6 +2695,8 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2685
2695
  url_path="rear-ports/add",
2686
2696
  url_name="bulk_add_rearport",
2687
2697
  component_model=RearPort,
2698
+ custom_view_base_action="change",
2699
+ custom_view_additional_permissions=["dcim.add_rearport"],
2688
2700
  )
2689
2701
  def bulk_add_rearport(self, request, *args, **kwargs):
2690
2702
  return self._bulk_component_create(
@@ -2699,6 +2711,8 @@ class ModuleUIViewSet(BulkComponentCreateUIViewSetMixin, NautobotUIViewSet):
2699
2711
  url_path="module-bays/add",
2700
2712
  url_name="bulk_add_modulebay",
2701
2713
  component_model=ModuleBay,
2714
+ custom_view_base_action="change",
2715
+ custom_view_additional_permissions=["dcim.add_modulebay"],
2702
2716
  )
2703
2717
  def bulk_add_modulebay(self, request, *args, **kwargs):
2704
2718
  return self._bulk_component_create(
@@ -3394,7 +3408,6 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3394
3408
  serializer_class = serializers.ModuleBaySerializer
3395
3409
  table_class = tables.ModuleBayTable
3396
3410
  create_template_name = "dcim/device_component_add.html"
3397
-
3398
3411
  object_detail_content = object_detail.ObjectDetailContent(
3399
3412
  panels=(
3400
3413
  object_detail.ObjectFieldsPanel(
@@ -3439,7 +3452,14 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3439
3452
  return parent.display
3440
3453
  return ""
3441
3454
 
3442
- @action(detail=False, methods=["GET", "POST"], url_path="rename", url_name="bulk_rename")
3455
+ @action(
3456
+ detail=False,
3457
+ methods=["GET", "POST"],
3458
+ url_path="rename",
3459
+ url_name="bulk_rename",
3460
+ custom_view_base_action="change",
3461
+ custom_view_additional_permissions=["dcim.change_modulebay"],
3462
+ )
3443
3463
  def bulk_rename(self, request, *args, **kwargs):
3444
3464
  return self._bulk_rename(request, *args, **kwargs)
3445
3465
 
@@ -3887,151 +3907,102 @@ class InterfaceConnectionsListView(ConnectionsListView):
3887
3907
  #
3888
3908
 
3889
3909
 
3890
- class VirtualChassisListView(generic.ObjectListView):
3891
- queryset = VirtualChassis.objects.all()
3892
- table = tables.VirtualChassisTable
3893
- filterset = filters.VirtualChassisFilterSet
3894
- filterset_form = forms.VirtualChassisFilterForm
3895
-
3896
-
3897
- class VirtualChassisView(generic.ObjectView):
3910
+ class VirtualChassisUIViewSet(NautobotUIViewSet):
3911
+ bulk_update_form_class = forms.VirtualChassisBulkEditForm
3912
+ filterset_class = filters.VirtualChassisFilterSet
3913
+ filterset_form_class = forms.VirtualChassisFilterForm
3914
+ form_class = forms.VirtualChassisCreateForm
3915
+ serializer_class = serializers.VirtualChassisSerializer
3916
+ table_class = tables.VirtualChassisTable
3898
3917
  queryset = VirtualChassis.objects.all()
3899
3918
 
3900
3919
  def get_extra_context(self, request, instance):
3901
- members = Device.objects.restrict(request.user).filter(virtual_chassis=instance)
3902
-
3903
- return {"members": members, **super().get_extra_context(request, instance)}
3904
-
3905
-
3906
- class VirtualChassisCreateView(generic.ObjectEditView):
3907
- queryset = VirtualChassis.objects.all()
3908
- model_form = forms.VirtualChassisCreateForm
3909
- template_name = "dcim/virtualchassis_add.html"
3910
-
3911
-
3912
- class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, View):
3913
- queryset = VirtualChassis.objects.all()
3914
-
3915
- def get_required_permission(self):
3916
- return "dcim.change_virtualchassis"
3917
-
3918
- def get(self, request, pk):
3919
- virtual_chassis = get_object_or_404(self.queryset, pk=pk)
3920
- VCMemberFormSet = modelformset_factory(
3921
- model=Device,
3922
- form=forms.DeviceVCMembershipForm,
3923
- formset=forms.BaseVCMemberFormSet,
3924
- extra=0,
3925
- )
3926
- members_queryset = virtual_chassis.members.select_related("rack").order_by("vc_position")
3927
-
3928
- vc_form = forms.VirtualChassisForm(instance=virtual_chassis)
3929
- vc_form.fields["master"].queryset = members_queryset
3930
- formset = VCMemberFormSet(queryset=members_queryset)
3931
-
3932
- return render(
3933
- request,
3934
- "dcim/virtualchassis_edit.html",
3935
- {
3936
- "vc_form": vc_form,
3937
- "formset": formset,
3938
- "return_url": self.get_return_url(request, virtual_chassis),
3939
- },
3940
- )
3941
-
3942
- def post(self, request, pk):
3943
- virtual_chassis = get_object_or_404(self.queryset, pk=pk)
3944
- VCMemberFormSet = modelformset_factory(
3945
- model=Device,
3946
- form=forms.DeviceVCMembershipForm,
3947
- formset=forms.BaseVCMemberFormSet,
3948
- extra=0,
3949
- )
3950
- members_queryset = virtual_chassis.members.select_related("rack").order_by("vc_position")
3951
-
3952
- vc_form = forms.VirtualChassisForm(request.POST, instance=virtual_chassis)
3953
- vc_form.fields["master"].queryset = members_queryset
3954
- formset = VCMemberFormSet(request.POST, queryset=members_queryset)
3955
-
3956
- if vc_form.is_valid() and formset.is_valid():
3957
- with transaction.atomic():
3958
- # Save the VirtualChassis
3959
- vc_form.save()
3960
-
3961
- # Nullify the vc_position of each member first to allow reordering without raising an IntegrityError on
3962
- # duplicate positions. Then save each member instance.
3963
- members = formset.save(commit=False)
3964
- devices = Device.objects.filter(pk__in=[m.pk for m in members])
3965
- for device in devices:
3966
- device.vc_position = None
3967
- device.save()
3968
- for member in members:
3969
- member.save()
3970
-
3971
- return redirect(virtual_chassis.get_absolute_url())
3972
-
3973
- return render(
3974
- request,
3975
- "dcim/virtualchassis_edit.html",
3976
- {
3977
- "vc_form": vc_form,
3978
- "formset": formset,
3979
- "return_url": self.get_return_url(request, virtual_chassis),
3980
- },
3981
- )
3982
-
3983
-
3984
- class VirtualChassisDeleteView(generic.ObjectDeleteView):
3985
- queryset = VirtualChassis.objects.all()
3920
+ context = super().get_extra_context(request, instance)
3986
3921
 
3922
+ if self.action == "update":
3923
+ VCMemberFormSet = modelformset_factory(
3924
+ model=Device,
3925
+ form=forms.DeviceVCMembershipForm,
3926
+ formset=forms.BaseVCMemberFormSet,
3927
+ extra=0,
3928
+ )
3929
+ members_queryset = instance.members.select_related("rack").order_by("vc_position")
3987
3930
 
3988
- class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMixin, View):
3989
- queryset = VirtualChassis.objects.all()
3931
+ if request.method == "POST":
3932
+ formset = VCMemberFormSet(request.POST, queryset=members_queryset)
3933
+ else:
3934
+ formset = VCMemberFormSet(queryset=members_queryset)
3990
3935
 
3991
- def get_required_permission(self):
3992
- return "dcim.change_virtualchassis"
3936
+ vc_form = forms.VirtualChassisForm(instance=instance)
3937
+ vc_form.fields["master"].queryset = members_queryset
3993
3938
 
3994
- def get(self, request, pk):
3995
- virtual_chassis = get_object_or_404(self.queryset, pk=pk)
3939
+ context.update(
3940
+ {
3941
+ "formset": formset,
3942
+ "vc_form": vc_form,
3943
+ "return_url": self.get_return_url(request, instance),
3944
+ }
3945
+ )
3996
3946
 
3997
- initial_data = {k: request.GET[k] for k in request.GET}
3998
- member_select_form = forms.VCMemberSelectForm(initial=initial_data)
3999
- membership_form = forms.DeviceVCMembershipForm(initial=initial_data)
3947
+ elif self.action == "retrieve":
3948
+ members = Device.objects.restrict(request.user).filter(virtual_chassis=instance)
3949
+ context.update({"members": members})
4000
3950
 
4001
- return render(
4002
- request,
4003
- "dcim/virtualchassis_add_member.html",
4004
- {
4005
- "virtual_chassis": virtual_chassis,
4006
- "member_select_form": member_select_form,
4007
- "membership_form": membership_form,
4008
- "return_url": self.get_return_url(request, virtual_chassis),
4009
- },
4010
- )
3951
+ return context
4011
3952
 
4012
- def post(self, request, pk):
4013
- virtual_chassis = get_object_or_404(self.queryset, pk=pk)
3953
+ def form_save(self, form, **kwargs):
3954
+ obj = super().form_save(form, **kwargs)
4014
3955
 
4015
- member_select_form = forms.VCMemberSelectForm(request.POST)
3956
+ if self.action == "update":
3957
+ context = self.get_extra_context(self.request, obj)
3958
+ formset = context.get("formset")
4016
3959
 
4017
- if member_select_form.is_valid():
4018
- device = member_select_form.cleaned_data["device"]
4019
- device.virtual_chassis = virtual_chassis
4020
- data = {k: request.POST[k] for k in ["vc_position", "vc_priority"]}
4021
- membership_form = forms.DeviceVCMembershipForm(data=data, validate_vc_position=True, instance=device)
3960
+ if formset.is_valid():
3961
+ with transaction.atomic():
3962
+ members = formset.save(commit=False)
3963
+ # Nullify the vc_position of each member first to allow reordering without raising an IntegrityError on
3964
+ # duplicate positions. Then save each member instance.
3965
+ Device.objects.filter(pk__in=[m.pk for m in members]).update(vc_position=None)
3966
+ for member in members:
3967
+ member.save()
3968
+ else:
3969
+ raise ValidationError(formset.errors)
4022
3970
 
4023
- if membership_form.is_valid():
4024
- membership_form.save()
4025
- msg = format_html('Added member <a href="{}">{}</a>', device.get_absolute_url(), device)
4026
- messages.success(request, msg)
3971
+ return obj
4027
3972
 
4028
- if "_addanother" in request.POST:
4029
- return redirect(request.get_full_path())
3973
+ @action(
3974
+ detail=True,
3975
+ methods=["get", "post"],
3976
+ url_path="add-member",
3977
+ url_name="add_member",
3978
+ custom_view_base_action="change",
3979
+ custom_view_additional_permissions=["dcim.change_virtualchassis"],
3980
+ )
3981
+ def add_member(self, request, pk=None):
3982
+ virtual_chassis = self.get_object()
4030
3983
 
4031
- return redirect(self.get_return_url(request, device))
3984
+ if request.method == "POST":
3985
+ member_select_form = forms.VCMemberSelectForm(request.POST)
3986
+ if member_select_form.is_valid():
3987
+ device = member_select_form.cleaned_data["device"]
3988
+ device.virtual_chassis = virtual_chassis
3989
+ data = {k: request.POST[k] for k in ["vc_position", "vc_priority"]}
3990
+ membership_form = forms.DeviceVCMembershipForm(data=data, validate_vc_position=True, instance=device)
3991
+
3992
+ if membership_form.is_valid():
3993
+ membership_form.save()
3994
+ msg = format_html('Added member <a href="{}">{}</a>', device.get_absolute_url(), device)
3995
+ messages.success(request, msg)
4032
3996
 
3997
+ if "_addanother" in request.POST:
3998
+ return redirect(request.get_full_path())
3999
+ return redirect(self.get_return_url(request, device))
4000
+ else:
4001
+ membership_form = forms.DeviceVCMembershipForm(data=request.POST)
4033
4002
  else:
4034
- membership_form = forms.DeviceVCMembershipForm(data=request.POST)
4003
+ initial_data = {k: request.GET[k] for k in request.GET}
4004
+ member_select_form = forms.VCMemberSelectForm(initial=initial_data)
4005
+ membership_form = forms.DeviceVCMembershipForm(initial=initial_data)
4035
4006
 
4036
4007
  return render(
4037
4008
  request,
@@ -4100,24 +4071,6 @@ class VirtualChassisRemoveMemberView(ObjectPermissionRequiredMixin, GetReturnURL
4100
4071
  )
4101
4072
 
4102
4073
 
4103
- class VirtualChassisBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
4104
- queryset = VirtualChassis.objects.all()
4105
- table = tables.VirtualChassisTable
4106
-
4107
-
4108
- class VirtualChassisBulkEditView(generic.BulkEditView):
4109
- queryset = VirtualChassis.objects.all()
4110
- filterset = filters.VirtualChassisFilterSet
4111
- table = tables.VirtualChassisTable
4112
- form = forms.VirtualChassisBulkEditForm
4113
-
4114
-
4115
- class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
4116
- queryset = VirtualChassis.objects.all()
4117
- filterset = filters.VirtualChassisFilterSet
4118
- table = tables.VirtualChassisTable
4119
-
4120
-
4121
4074
  #
4122
4075
  # Power panels
4123
4076
  #
@@ -4159,6 +4112,28 @@ class PowerPanelUIViewSet(NautobotUIViewSet):
4159
4112
  #
4160
4113
  # Power feeds
4161
4114
  #
4115
+
4116
+
4117
+ class CustomPowerFeedKeyValueTablePanel(object_detail.KeyValueTablePanel):
4118
+ """Custom panel to render PowerFeed utilization graph cleanly."""
4119
+
4120
+ def render_value(self, key, value, context):
4121
+ if key == "Utilization (Allocated)":
4122
+ if not value or not isinstance(value, tuple) or len(value) != 2:
4123
+ return helpers.placeholder(None)
4124
+ allocated, available = value
4125
+ if available <= 0:
4126
+ return f"{allocated}VA / {available}VA"
4127
+ graph_html = render_to_string(
4128
+ "utilities/templatetags/utilization_graph.html",
4129
+ helpers.utilization_graph_raw_data(allocated, available),
4130
+ )
4131
+ return format_html("{}VA / {}VA {}", allocated, available, graph_html)
4132
+
4133
+ # Fall back to default behavior for everything else
4134
+ return super().render_value(key, value, context)
4135
+
4136
+
4162
4137
  class PowerFeedUIViewSet(NautobotUIViewSet):
4163
4138
  bulk_update_form_class = forms.PowerFeedBulkEditForm
4164
4139
  filterset_class = filters.PowerFeedFilterSet
@@ -4168,6 +4143,137 @@ class PowerFeedUIViewSet(NautobotUIViewSet):
4168
4143
  serializer_class = serializers.PowerFeedSerializer
4169
4144
  table_class = tables.PowerFeedTable
4170
4145
 
4146
+ object_detail_content = object_detail.ObjectDetailContent(
4147
+ panels=(
4148
+ CustomPowerFeedKeyValueTablePanel(
4149
+ section=SectionChoices.LEFT_HALF,
4150
+ weight=100,
4151
+ label="Power Feed",
4152
+ context_data_key="powerfeed_data",
4153
+ ),
4154
+ object_detail.ObjectFieldsPanel(
4155
+ section=SectionChoices.LEFT_HALF,
4156
+ weight=200,
4157
+ label="Electrical Characteristics",
4158
+ fields=["supply", "voltage", "amperage", "phase", "max_utilization"],
4159
+ value_transforms={
4160
+ "voltage": [lambda v: f"{v}V" if v is not None else helpers.placeholder(v)],
4161
+ "amperage": [lambda a: f"{a}A" if a is not None else helpers.placeholder(a)],
4162
+ "max_utilization": [lambda p: f"{p}%" if p is not None else helpers.placeholder(p)],
4163
+ },
4164
+ ),
4165
+ object_detail.KeyValueTablePanel(
4166
+ section=SectionChoices.RIGHT_HALF,
4167
+ weight=300,
4168
+ label="Connection",
4169
+ context_data_key="connection_data",
4170
+ ),
4171
+ )
4172
+ )
4173
+
4174
+ def get_extra_context(self, request, instance):
4175
+ context = super().get_extra_context(request, instance)
4176
+ if not instance or self.action != "retrieve":
4177
+ return context
4178
+
4179
+ context["powerfeed_data"] = {
4180
+ "Power Panel": instance.power_panel,
4181
+ "Rack": instance.rack,
4182
+ "Type": self._get_type_html(instance), # Render Type with HTML label
4183
+ "Status": instance.status,
4184
+ "Connected Device": self._get_connected_device_html(instance),
4185
+ "Utilization (Allocated)": self._get_utilization_data(instance),
4186
+ }
4187
+
4188
+ context["connection_data"] = self._get_connection_data(request, instance)
4189
+ return context
4190
+
4191
+ def _get_type_html(self, instance):
4192
+ """
4193
+ Render the PowerFeed type as a label with the appropriate CSS class.
4194
+ """
4195
+
4196
+ type_class = instance.get_type_class()
4197
+ return format_html('<span class="label label-{}">{}</span>', type_class, instance.get_type_display())
4198
+
4199
+ def _get_connected_device_html(self, instance):
4200
+ endpoint = getattr(instance, "connected_endpoint", None)
4201
+ if endpoint and endpoint.parent:
4202
+ parent = helpers.hyperlinked_object(endpoint.parent)
4203
+ return format_html("{} ({})", parent, endpoint)
4204
+ return None
4205
+
4206
+ def _get_utilization_data(self, instance):
4207
+ endpoint = getattr(instance, "connected_endpoint", None)
4208
+ if not endpoint or not hasattr(endpoint, "get_power_draw"):
4209
+ return None
4210
+ utilization = endpoint.get_power_draw()
4211
+ if not utilization or "allocated" not in utilization:
4212
+ return None
4213
+ allocated = utilization["allocated"]
4214
+ available = instance.available_power or 0
4215
+ return (allocated, available)
4216
+
4217
+ def _get_connection_data(self, request, instance):
4218
+ if not instance:
4219
+ return {}
4220
+
4221
+ if instance.cable:
4222
+ trace_url = reverse("dcim:powerfeed_trace", kwargs={"pk": instance.pk})
4223
+ cable_html = format_html(
4224
+ '{} <a href="{}" class="btn btn-primary btn-xs" title="Trace">'
4225
+ '<i class="mdi mdi-transit-connection-variant"></i></a>',
4226
+ helpers.hyperlinked_object(instance.cable),
4227
+ trace_url,
4228
+ )
4229
+
4230
+ endpoint = getattr(instance, "connected_endpoint", None)
4231
+ endpoint_data = {}
4232
+
4233
+ if endpoint:
4234
+ endpoint_obj = getattr(endpoint, "device", None) or getattr(endpoint, "module", None)
4235
+ # Removed the unused 'path' variable
4236
+ endpoint_data = {
4237
+ "Device" if getattr(endpoint, "device", None) else "Module": endpoint_obj,
4238
+ "Power Port": endpoint,
4239
+ "Type": endpoint.get_type_display() if hasattr(endpoint, "get_type_display") else None,
4240
+ "Description": endpoint.description,
4241
+ "Path Status": self._get_path_status_html(instance), # Render Path Status dynamically
4242
+ }
4243
+
4244
+ return {
4245
+ "Cable": cable_html,
4246
+ **endpoint_data,
4247
+ }
4248
+
4249
+ if request.user.has_perm("dcim.add_cable"):
4250
+ connect_url = (
4251
+ reverse(
4252
+ "dcim:powerfeed_connect",
4253
+ kwargs={"termination_a_id": instance.pk, "termination_b_type": "power-port"},
4254
+ )
4255
+ + f"?return_url={instance.get_absolute_url()}"
4256
+ )
4257
+ connect_link = format_html(
4258
+ '<a href="{}" class="btn btn-primary btn-sm pull-right">'
4259
+ '<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect</a>',
4260
+ connect_url,
4261
+ )
4262
+ return {"Connection": format_html("Not connected {}", connect_link)}
4263
+
4264
+ return {"Connection": "Not connected"}
4265
+
4266
+ def _get_path_status_html(self, instance):
4267
+ """
4268
+ Render the Path Status as a label based on the path status (active or not).
4269
+ """
4270
+ path_status = (
4271
+ '<span class="label label-success">Reachable</span>'
4272
+ if getattr(instance, "path", None) and instance.path.is_active
4273
+ else '<span class="label label-danger">Not Reachable</span>'
4274
+ )
4275
+ return format_html(path_status) # Safely render HTML
4276
+
4171
4277
 
4172
4278
  class DeviceRedundancyGroupUIViewSet(NautobotUIViewSet):
4173
4279
  bulk_update_form_class = forms.DeviceRedundancyGroupBulkEditForm
@@ -4218,38 +4324,35 @@ class InterfaceRedundancyGroupUIViewSet(NautobotUIViewSet):
4218
4324
  table_class = tables.InterfaceRedundancyGroupTable
4219
4325
  lookup_field = "pk"
4220
4326
 
4221
- def get_extra_context(self, request, instance):
4222
- """Return additional panels for display."""
4223
- context = super().get_extra_context(request, instance)
4224
- if instance and self.action == "retrieve":
4225
- interface_table = self._get_interface_redundancy_groups_table(request, instance)
4226
- context["interface_table"] = interface_table
4227
- return context
4228
-
4229
- def _get_interface_redundancy_groups_table(self, request, instance):
4230
- """Return a table of assigned Interfaces."""
4231
- queryset = instance.interface_redundancy_group_associations.restrict(request.user)
4232
- queryset = queryset.prefetch_related("interface")
4233
- queryset = queryset.order_by("priority")
4234
- column_sequence = (
4235
- "interface__device",
4236
- "interface",
4237
- "priority",
4238
- "interface__status",
4239
- "interface__enabled",
4240
- "interface__ip_addresses",
4241
- "interface__type",
4242
- "interface__description",
4243
- "interface__label",
4244
- )
4245
- table = tables.InterfaceRedundancyGroupAssociationTable(
4246
- data=queryset,
4247
- sequence=column_sequence,
4248
- orderable=False,
4249
- )
4250
- for column_name in column_sequence:
4251
- table.columns.show(column_name)
4252
- return table
4327
+ object_detail_content = object_detail.ObjectDetailContent(
4328
+ panels=(
4329
+ object_detail.ObjectFieldsPanel(
4330
+ weight=100,
4331
+ section=SectionChoices.LEFT_HALF,
4332
+ fields="__all__",
4333
+ ),
4334
+ object_detail.ObjectsTablePanel(
4335
+ weight=200,
4336
+ section=SectionChoices.FULL_WIDTH,
4337
+ table_class=tables.InterfaceRedundancyGroupAssociationTable,
4338
+ table_attribute="interface_redundancy_group_associations",
4339
+ prefetch_related_fields=["interface"],
4340
+ order_by_fields=["priority"],
4341
+ table_title="Interfaces",
4342
+ related_field_name="interface_redundancy_group",
4343
+ include_columns=[
4344
+ "interface__device",
4345
+ "interface",
4346
+ "interface__status",
4347
+ "interface__enabled",
4348
+ "interface__ip_addresses",
4349
+ "interface__type",
4350
+ "interface__description",
4351
+ "interface__label",
4352
+ ],
4353
+ ),
4354
+ ),
4355
+ )
4253
4356
 
4254
4357
 
4255
4358
  class InterfaceRedundancyGroupAssociationUIViewSet(ObjectEditViewMixin, ObjectDestroyViewMixin):
@@ -4259,6 +4362,51 @@ class InterfaceRedundancyGroupAssociationUIViewSet(ObjectEditViewMixin, ObjectDe
4259
4362
  lookup_field = "pk"
4260
4363
 
4261
4364
 
4365
+ class ModuleFamilyUIViewSet(NautobotUIViewSet):
4366
+ """ViewSet for the ModuleFamily model."""
4367
+
4368
+ filterset_class = filters.ModuleFamilyFilterSet
4369
+ filterset_form_class = forms.ModuleFamilyFilterForm
4370
+ form_class = forms.ModuleFamilyForm
4371
+ bulk_update_form_class = forms.ModuleFamilyBulkEditForm
4372
+ queryset = ModuleFamily.objects.all()
4373
+ serializer_class = serializers.ModuleFamilySerializer
4374
+ table_class = tables.ModuleFamilyTable
4375
+ lookup_field = "pk"
4376
+
4377
+ def get_extra_context(self, request, instance):
4378
+ context = super().get_extra_context(request, instance)
4379
+ if not instance:
4380
+ return context
4381
+
4382
+ if self.action == "retrieve":
4383
+ module_types = (
4384
+ ModuleType.objects.restrict(request.user, "view")
4385
+ .filter(module_family=instance)
4386
+ .select_related("manufacturer")
4387
+ )
4388
+ module_type_table = tables.ModuleTypeTable(module_types, orderable=False)
4389
+
4390
+ module_bays = (
4391
+ ModuleBay.objects.restrict(request.user, "view")
4392
+ .filter(module_family=instance)
4393
+ .select_related("parent_device", "parent_module")
4394
+ )
4395
+ module_bay_table = tables.ModuleBayTable(module_bays, orderable=False)
4396
+
4397
+ paginate = {
4398
+ "paginator_class": EnhancedPaginator,
4399
+ "per_page": get_paginate_count(request),
4400
+ }
4401
+ RequestConfig(request, paginate).configure(module_type_table)
4402
+ RequestConfig(request, paginate).configure(module_bay_table)
4403
+
4404
+ context["module_type_table"] = module_type_table
4405
+ context["module_bay_table"] = module_bay_table
4406
+
4407
+ return context
4408
+
4409
+
4262
4410
  class DeviceFamilyUIViewSet(NautobotUIViewSet):
4263
4411
  filterset_class = filters.DeviceFamilyFilterSet
4264
4412
  filterset_form_class = forms.DeviceFamilyFilterForm
@@ -4308,7 +4456,6 @@ class SoftwareImageFileUIViewSet(NautobotUIViewSet):
4308
4456
  queryset = SoftwareImageFile.objects.all()
4309
4457
  serializer_class = serializers.SoftwareImageFileSerializer
4310
4458
  table_class = tables.SoftwareImageFileTable
4311
-
4312
4459
  object_detail_content = object_detail.ObjectDetailContent(
4313
4460
  panels=(
4314
4461
  object_detail.ObjectFieldsPanel(
@@ -4392,19 +4539,42 @@ class SoftwareImageFileUIViewSet(NautobotUIViewSet):
4392
4539
  ),
4393
4540
  )
4394
4541
 
4395
- @action(detail=True, url_path="device-types", url_name="device_types")
4542
+ @action(
4543
+ detail=True,
4544
+ url_path="device-types",
4545
+ url_name="device_types",
4546
+ custom_view_base_action="view",
4547
+ custom_view_additional_permissions=["dcim.view_devicetype"],
4548
+ )
4396
4549
  def device_types(self, request, *args, **kwargs):
4397
4550
  return Response({})
4398
4551
 
4399
- @action(detail=True, url_path="devices")
4552
+ @action(
4553
+ detail=True,
4554
+ url_path="devices",
4555
+ custom_view_base_action="view",
4556
+ custom_view_additional_permissions=["dcim.view_device"],
4557
+ )
4400
4558
  def devices(self, request, *args, **kwargs):
4401
4559
  return Response({})
4402
4560
 
4403
- @action(detail=True, url_path="inventory-items", url_name="inventory_items")
4561
+ @action(
4562
+ detail=True,
4563
+ url_path="inventory-items",
4564
+ url_name="inventory_items",
4565
+ custom_view_base_action="view",
4566
+ custom_view_additional_permissions=["dcim.view_inventoryitem"],
4567
+ )
4404
4568
  def inventory_items(self, request, *args, **kwargs):
4405
4569
  return Response({})
4406
4570
 
4407
- @action(detail=True, url_path="virtual-machines", url_name="virtual_machines")
4571
+ @action(
4572
+ detail=True,
4573
+ url_path="virtual-machines",
4574
+ url_name="virtual_machines",
4575
+ custom_view_base_action="view",
4576
+ custom_view_additional_permissions=["virtualization.view_virtualmachine"],
4577
+ )
4408
4578
  def virtual_machines(self, request, *args, **kwargs):
4409
4579
  return Response({})
4410
4580
 
@@ -4450,54 +4620,80 @@ class ControllerUIViewSet(NautobotUIViewSet):
4450
4620
  table_class = tables.ControllerTable
4451
4621
  template_name = "dcim/controller_create.html"
4452
4622
 
4453
- def get_extra_context(self, request, instance):
4454
- context = super().get_extra_context(request, instance)
4455
-
4456
- if self.action == "retrieve" and instance:
4457
- devices = Device.objects.restrict(request.user).filter(controller_managed_device_group__controller=instance)
4458
- devices_table = tables.DeviceTable(devices)
4459
- devices_table.columns.show("controller_managed_device_group")
4460
- devices_table.columns.show("capabilities")
4461
- devices_table.sequence = ("name", "controller_managed_device_group", "capabilities", "...")
4462
-
4463
- paginate = {
4464
- "paginator_class": EnhancedPaginator,
4465
- "per_page": get_paginate_count(request),
4466
- }
4467
- RequestConfig(request, paginate).configure(devices_table)
4468
-
4469
- context["devices_table"] = devices_table
4470
-
4471
- return context
4623
+ object_detail_content = object_detail.ObjectDetailContent(
4624
+ panels=(
4625
+ object_detail.ObjectFieldsPanel(
4626
+ section=SectionChoices.LEFT_HALF,
4627
+ weight=100,
4628
+ fields="__all__",
4629
+ value_transforms={
4630
+ "capabilities": [helpers.label_list],
4631
+ },
4632
+ exclude_fields=[
4633
+ "external_integration",
4634
+ "controller_device",
4635
+ "controller_device_redundancy_group",
4636
+ ],
4637
+ ),
4638
+ object_detail.ObjectFieldsPanel(
4639
+ section=SectionChoices.RIGHT_HALF,
4640
+ weight=200,
4641
+ label="Integration",
4642
+ fields=[
4643
+ "external_integration",
4644
+ "controller_device",
4645
+ "controller_device_redundancy_group",
4646
+ ],
4647
+ ),
4648
+ object_detail.ObjectsTablePanel(
4649
+ section=SectionChoices.FULL_WIDTH,
4650
+ weight=100,
4651
+ table_class=tables.DeviceTable,
4652
+ table_title="Managed Devices",
4653
+ table_filter="controller_managed_device_group__controller",
4654
+ include_columns=[
4655
+ "capabilities",
4656
+ "controller_managed_device_group",
4657
+ "manufacturer",
4658
+ ],
4659
+ related_field_name="controller",
4660
+ add_button_route=None,
4661
+ ),
4662
+ ),
4663
+ extra_tabs=(
4664
+ object_detail.DistinctViewTab(
4665
+ weight=700,
4666
+ tab_id="wireless_networks",
4667
+ url_name="dcim:controller_wirelessnetworks",
4668
+ label="Wireless Networks",
4669
+ related_object_attribute="wireless_network_assignments",
4670
+ panels=(
4671
+ object_detail.ObjectsTablePanel(
4672
+ section=SectionChoices.FULL_WIDTH,
4673
+ weight=100,
4674
+ table_title="Wireless Networks",
4675
+ table_class=BaseControllerManagedDeviceGroupWirelessNetworkAssignmentTable,
4676
+ table_filter="controller_managed_device_group__controller",
4677
+ tab_id="wireless_networks",
4678
+ add_button_route=None,
4679
+ select_related_fields=["wireless_network"],
4680
+ exclude_columns=["controller"],
4681
+ ),
4682
+ ),
4683
+ ),
4684
+ ),
4685
+ )
4472
4686
 
4473
- @action(detail=True, url_path="wireless-networks", url_name="wirelessnetworks", methods=["get"])
4687
+ @action(
4688
+ detail=True,
4689
+ url_path="wireless-networks",
4690
+ url_name="wirelessnetworks",
4691
+ methods=["get"],
4692
+ custom_view_base_action="view",
4693
+ custom_view_additional_permissions=["wireless.view_controllermanageddevicegroupwirelessnetworkassignment"],
4694
+ )
4474
4695
  def wirelessnetworks(self, request, *args, **kwargs):
4475
- instance = self.get_object()
4476
- controller_managed_device_groups = instance.controller_managed_device_groups.restrict(
4477
- request.user, "view"
4478
- ).values_list("pk", flat=True)
4479
- wireless_networks = ControllerManagedDeviceGroupWirelessNetworkAssignment.objects.filter(
4480
- controller_managed_device_group__in=list(controller_managed_device_groups)
4481
- ).select_related("wireless_network")
4482
- wireless_networks_table = ControllerManagedDeviceGroupWirelessNetworkAssignmentTable(
4483
- data=wireless_networks, user=request.user, orderable=False
4484
- )
4485
- wireless_networks_table.columns.hide("controller")
4486
-
4487
- RequestConfig(
4488
- request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
4489
- ).configure(wireless_networks_table)
4490
-
4491
- return Response(
4492
- {
4493
- "wireless_networks_table": wireless_networks_table,
4494
- "active_tab": "wireless-networks",
4495
- }
4496
- )
4497
-
4498
- def get_action(self):
4499
- "Treat Wireless Networks as the same detail view for permission purposes."
4500
- return "view" if self.action == "wirelessnetworks" else super().get_action()
4696
+ return Response({})
4501
4697
 
4502
4698
 
4503
4699
  class ControllerManagedDeviceGroupUIViewSet(NautobotUIViewSet):
@@ -4509,42 +4705,88 @@ class ControllerManagedDeviceGroupUIViewSet(NautobotUIViewSet):
4509
4705
  serializer_class = serializers.ControllerManagedDeviceGroupSerializer
4510
4706
  table_class = tables.ControllerManagedDeviceGroupTable
4511
4707
  template_name = "dcim/controllermanageddevicegroup_create.html"
4708
+ object_detail_content = object_detail.ObjectDetailContent(
4709
+ panels=(
4710
+ object_detail.ObjectFieldsPanel(
4711
+ section=SectionChoices.LEFT_HALF,
4712
+ weight=100,
4713
+ fields="__all__",
4714
+ value_transforms={
4715
+ "capabilities": [helpers.label_list],
4716
+ },
4717
+ ),
4718
+ object_detail.ObjectsTablePanel(
4719
+ section=SectionChoices.FULL_WIDTH,
4720
+ weight=100,
4721
+ table_class=tables.DeviceTable,
4722
+ table_filter="controller_managed_device_group",
4723
+ add_button_route=None,
4724
+ ),
4725
+ ),
4726
+ extra_tabs=(
4727
+ object_detail.DistinctViewTab(
4728
+ weight=800,
4729
+ tab_id="wireless_networks",
4730
+ label="Wireless Networks",
4731
+ url_name="dcim:controllermanageddevicegroup_wireless_networks",
4732
+ related_object_attribute="wireless_network_assignments",
4733
+ panels=(
4734
+ object_detail.ObjectsTablePanel(
4735
+ section=SectionChoices.FULL_WIDTH,
4736
+ weight=100,
4737
+ table_title="Wireless Networks",
4738
+ table_class=DeviceGroupWirelessNetworkTable,
4739
+ table_filter="controller_managed_device_group",
4740
+ related_field_name="controller_managed_device_groups",
4741
+ tab_id="wireless_networks",
4742
+ add_button_route=None,
4743
+ exclude_columns=["controller_managed_device_group", "controller"],
4744
+ ),
4745
+ ),
4746
+ ),
4747
+ object_detail.DistinctViewTab(
4748
+ weight=900,
4749
+ tab_id="radio_profiles",
4750
+ label="Radio Profiles",
4751
+ url_name="dcim:controllermanageddevicegroup_radio_profiles",
4752
+ related_object_attribute="radio_profiles",
4753
+ panels=(
4754
+ object_detail.ObjectsTablePanel(
4755
+ section=SectionChoices.FULL_WIDTH,
4756
+ weight=100,
4757
+ table_title="Radio Profiles",
4758
+ table_class=RadioProfileTable,
4759
+ table_filter="controller_managed_device_groups",
4760
+ tab_id="radio_profiles",
4761
+ add_button_route=None,
4762
+ ),
4763
+ ),
4764
+ ),
4765
+ ),
4766
+ )
4512
4767
 
4513
- def get_extra_context(self, request, instance):
4514
- context = super().get_extra_context(request, instance)
4515
-
4516
- if self.action == "retrieve" and instance:
4517
- devices = instance.devices.restrict(request.user)
4518
- devices_table = tables.DeviceTable(devices)
4768
+ @action(
4769
+ detail=True,
4770
+ url_path="wireless-networks",
4771
+ url_name="wireless_networks",
4772
+ custom_view_base_action="view",
4773
+ custom_view_additional_permissions=["wireless.view_controllermanageddevicegroupwirelessnetworkassignment"],
4774
+ )
4775
+ def wireless_networks(self, request, *args, **kwargs):
4776
+ return Response({})
4519
4777
 
4520
- paginate = {
4521
- "paginator_class": EnhancedPaginator,
4522
- "per_page": get_paginate_count(request),
4523
- }
4524
- RequestConfig(request, paginate).configure(devices_table)
4525
-
4526
- context["devices_table"] = devices_table
4527
-
4528
- # Wireless Networks
4529
- wireless_networks = instance.wireless_network_assignments.restrict(request.user, "view")
4530
- wireless_networks_table = ControllerManagedDeviceGroupWirelessNetworkAssignmentTable(wireless_networks)
4531
- wireless_networks_table.columns.hide("controller_managed_device_group")
4532
- wireless_networks_table.columns.hide("controller")
4533
- RequestConfig(
4534
- request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
4535
- ).configure(wireless_networks_table)
4536
- context["wireless_networks_table"] = wireless_networks_table
4537
- context["wireless_networks_count"] = wireless_networks.count()
4538
-
4539
- # Radio Profiles
4540
- radio_profiles = instance.radio_profiles.restrict(request.user, "view")
4541
- radio_profiles_table = RadioProfileTable(radio_profiles)
4542
- RequestConfig(
4543
- request, paginate={"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
4544
- ).configure(radio_profiles_table)
4545
- context["radio_profiles_table"] = radio_profiles_table
4546
- context["radio_profiles_count"] = radio_profiles.count()
4778
+ @action(
4779
+ detail=True,
4780
+ url_path="radio-profiles",
4781
+ url_name="radio_profiles",
4782
+ custom_view_base_action="view",
4783
+ custom_view_additional_permissions=["wireless.view_radioprofile"],
4784
+ )
4785
+ def radio_profiles(self, request, *args, **kwargs):
4786
+ return Response({})
4547
4787
 
4788
+ def get_extra_context(self, request, instance):
4789
+ context = super().get_extra_context(request, instance)
4548
4790
  if self.action in ["create", "update"]:
4549
4791
  context["wireless_networks"] = ControllerManagedDeviceGroupWirelessNetworkFormSet(
4550
4792
  instance=instance,