nautobot 2.4.14__py3-none-any.whl → 2.4.16__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.
Files changed (434) hide show
  1. nautobot/apps/choices.py +8 -0
  2. nautobot/apps/ui.py +14 -0
  3. nautobot/core/api/views.py +2 -0
  4. nautobot/core/choices.py +4 -0
  5. nautobot/core/filters.py +21 -41
  6. nautobot/core/management/commands/check_job_approval_status.py +47 -0
  7. nautobot/core/management/commands/generate_test_data.py +1 -1
  8. nautobot/core/management/commands/migrate.py +1 -1
  9. nautobot/core/models/tree_queries.py +17 -0
  10. nautobot/core/settings.py +2 -2
  11. nautobot/core/tables.py +25 -2
  12. nautobot/core/templates/base_django.html +1 -1
  13. nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
  14. nautobot/core/templates/generic/object_list.html +17 -20
  15. nautobot/core/templates/inc/breadcrumbs.html +14 -0
  16. nautobot/core/templatetags/buttons.py +2 -4
  17. nautobot/core/templatetags/helpers.py +29 -6
  18. nautobot/core/templatetags/ui_framework.py +21 -0
  19. nautobot/core/testing/filters.py +20 -3
  20. nautobot/core/testing/forms.py +1 -1
  21. nautobot/core/tests/integration/test_filters.py +2 -2
  22. nautobot/core/tests/test_breadcrumbs.py +366 -0
  23. nautobot/core/tests/test_commands.py +40 -0
  24. nautobot/core/tests/test_filters.py +51 -1
  25. nautobot/core/tests/test_forms.py +1 -1
  26. nautobot/core/tests/test_graphql.py +4 -4
  27. nautobot/core/tests/test_titles.py +183 -0
  28. nautobot/core/tests/test_tree_queries.py +30 -0
  29. nautobot/core/tests/test_views.py +2 -2
  30. nautobot/core/tests/test_views_utils.py +1 -1
  31. nautobot/core/ui/breadcrumbs.py +538 -0
  32. nautobot/core/ui/bulk_buttons.py +53 -0
  33. nautobot/core/ui/object_detail.py +31 -8
  34. nautobot/core/ui/titles.py +127 -0
  35. nautobot/core/ui/utils.py +25 -0
  36. nautobot/core/utils/migrations.py +1 -1
  37. nautobot/core/views/__init__.py +1 -1
  38. nautobot/core/views/mixins.py +26 -1
  39. nautobot/core/views/renderers.py +20 -2
  40. nautobot/core/views/utils.py +13 -12
  41. nautobot/dcim/api/serializers.py +9 -0
  42. nautobot/dcim/choices.py +53 -0
  43. nautobot/dcim/filters/__init__.py +15 -3
  44. nautobot/dcim/forms.py +120 -7
  45. nautobot/dcim/management/commands/trace_paths.py +1 -1
  46. nautobot/dcim/migrations/0072_alter_powerfeed_options_and_more.py +97 -0
  47. nautobot/dcim/models/device_component_templates.py +8 -0
  48. nautobot/dcim/models/device_components.py +31 -12
  49. nautobot/dcim/models/devices.py +1 -1
  50. nautobot/dcim/models/power.py +171 -10
  51. nautobot/dcim/models/racks.py +7 -4
  52. nautobot/dcim/tables/devices.py +2 -0
  53. nautobot/dcim/tables/devicetypes.py +1 -0
  54. nautobot/dcim/tables/power.py +30 -2
  55. nautobot/dcim/templates/dcim/device.html +2 -2
  56. nautobot/dcim/templates/dcim/devicetype_retrieve.html +1 -214
  57. nautobot/dcim/templates/dcim/location_retrieve.html +2 -2
  58. nautobot/dcim/templates/dcim/powerfeed_edit.html +8 -0
  59. nautobot/dcim/templates/dcim/powerfeed_retrieve.html +1 -1
  60. nautobot/dcim/tests/integration/test_device_bulk_operations.py +61 -0
  61. nautobot/dcim/tests/test_api.py +24 -4
  62. nautobot/dcim/tests/test_filters.py +91 -13
  63. nautobot/dcim/tests/test_models.py +262 -0
  64. nautobot/dcim/tests/test_views.py +20 -12
  65. nautobot/dcim/utils.py +9 -0
  66. nautobot/dcim/views.py +390 -77
  67. nautobot/extras/factory.py +19 -20
  68. nautobot/extras/filters/__init__.py +3 -2
  69. nautobot/extras/filters/mixins.py +15 -1
  70. nautobot/extras/forms/__init__.py +2 -1
  71. nautobot/extras/forms/forms.py +62 -0
  72. nautobot/extras/managers.py +4 -1
  73. nautobot/extras/migrations/0125_jobresult_date_started.py +18 -0
  74. nautobot/extras/models/customfields.py +1 -2
  75. nautobot/extras/models/datasources.py +1 -2
  76. nautobot/extras/models/jobs.py +7 -3
  77. nautobot/extras/plugins/views.py +24 -1
  78. nautobot/extras/secrets/__init__.py +1 -1
  79. nautobot/extras/tables.py +21 -0
  80. nautobot/extras/templates/extras/customfield.html +2 -129
  81. nautobot/extras/templates/extras/customfield_edit.html +2 -108
  82. nautobot/extras/templates/extras/customfield_retrieve.html +129 -0
  83. nautobot/extras/templates/extras/customfield_update.html +108 -0
  84. nautobot/extras/templates/extras/inc/jobresult.html +7 -3
  85. nautobot/extras/templates/extras/jobresult.html +2 -155
  86. nautobot/extras/templates/extras/jobresult_retrieve.html +155 -0
  87. nautobot/extras/templates/extras/marketplace.html +5 -6
  88. nautobot/extras/templates/extras/note.html +2 -53
  89. nautobot/extras/templates/extras/note_retrieve.html +53 -0
  90. nautobot/extras/templates/extras/plugins_list.html +5 -6
  91. nautobot/extras/templates/extras/secretsgroup_retrieve.html +2 -29
  92. nautobot/extras/templatetags/custom_links.py +2 -2
  93. nautobot/extras/templatetags/job_buttons.py +1 -1
  94. nautobot/extras/templatetags/plugins.py +1 -1
  95. nautobot/extras/tests/integration/test_computedfields.py +2 -2
  96. nautobot/extras/tests/integration/test_customfields.py +14 -11
  97. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  98. nautobot/extras/tests/integration/test_notes.py +1 -1
  99. nautobot/extras/tests/integration/test_plugins.py +6 -6
  100. nautobot/extras/tests/integration/test_relationships.py +2 -2
  101. nautobot/extras/tests/test_filters.py +9 -0
  102. nautobot/extras/tests/test_forms.py +2 -2
  103. nautobot/extras/tests/test_plugins.py +14 -3
  104. nautobot/extras/tests/test_relationships.py +7 -7
  105. nautobot/extras/tests/test_views.py +172 -1
  106. nautobot/extras/urls.py +3 -59
  107. nautobot/extras/utils.py +1 -1
  108. nautobot/extras/views.py +96 -182
  109. nautobot/ipam/tables.py +8 -15
  110. nautobot/ipam/tests/migration/test_migrations.py +8 -8
  111. nautobot/ipam/tests/test_api.py +2 -2
  112. nautobot/ipam/tests/test_models.py +1 -1
  113. nautobot/project-static/docs/404.html +23 -0
  114. nautobot/project-static/docs/apps/index.html +23 -0
  115. nautobot/project-static/docs/apps/nautobot-apps.html +23 -0
  116. nautobot/project-static/docs/assets/_mkdocstrings.css +44 -6
  117. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +28 -0
  118. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +25 -0
  119. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +128 -20
  120. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +37 -4
  121. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +39 -6
  122. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +25 -0
  123. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +24 -0
  124. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +32 -5
  125. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +41 -8
  126. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +39 -7
  127. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +43 -10
  128. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +74 -59
  129. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +143 -28
  130. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +43 -12
  131. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +135 -53
  132. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +229 -36
  133. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +27 -1
  134. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +30 -1
  135. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +162 -18
  136. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +258 -51
  137. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +5987 -2620
  138. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +25 -0
  139. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +154 -55
  140. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +150 -35
  141. nautobot/project-static/docs/development/apps/api/configuration-view.html +23 -0
  142. nautobot/project-static/docs/development/apps/api/database-backend-config.html +23 -0
  143. nautobot/project-static/docs/development/apps/api/models/django-admin.html +23 -0
  144. nautobot/project-static/docs/development/apps/api/models/global-search.html +23 -0
  145. nautobot/project-static/docs/development/apps/api/models/graphql.html +23 -0
  146. nautobot/project-static/docs/development/apps/api/models/index.html +23 -0
  147. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +23 -0
  148. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +23 -0
  149. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +23 -0
  150. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +23 -0
  151. nautobot/project-static/docs/development/apps/api/platform-features/index.html +23 -0
  152. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +23 -0
  153. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +23 -0
  154. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +23 -0
  155. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +23 -0
  156. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +23 -0
  157. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +23 -0
  158. nautobot/project-static/docs/development/apps/api/prometheus.html +23 -0
  159. nautobot/project-static/docs/development/apps/api/setup.html +23 -0
  160. nautobot/project-static/docs/development/apps/api/testing.html +23 -0
  161. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +23 -0
  162. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +23 -0
  163. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +23 -0
  164. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +23 -0
  165. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +23 -0
  166. nautobot/project-static/docs/development/apps/api/views/base-template.html +23 -0
  167. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +23 -0
  168. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +23 -0
  169. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +23 -0
  170. nautobot/project-static/docs/development/apps/api/views/index.html +23 -0
  171. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +23 -0
  172. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -2
  173. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +23 -0
  174. nautobot/project-static/docs/development/apps/api/views/notes.html +23 -0
  175. nautobot/project-static/docs/development/apps/api/views/rest-api.html +23 -0
  176. nautobot/project-static/docs/development/apps/api/views/urls.html +23 -0
  177. nautobot/project-static/docs/development/apps/index.html +23 -0
  178. nautobot/project-static/docs/development/apps/migration/code-updates.html +23 -0
  179. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +23 -0
  180. nautobot/project-static/docs/development/apps/migration/from-v1.html +23 -0
  181. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +23 -0
  182. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +23 -0
  183. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +23 -0
  184. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +23 -0
  185. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +26 -3
  186. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +10544 -0
  187. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +23 -0
  188. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +23 -0
  189. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +23 -0
  190. nautobot/project-static/docs/development/apps/porting-from-netbox.html +26 -3
  191. nautobot/project-static/docs/development/core/application-registry.html +23 -0
  192. nautobot/project-static/docs/development/core/best-practices.html +23 -0
  193. nautobot/project-static/docs/development/core/bootstrap-ui.html +23 -0
  194. nautobot/project-static/docs/development/core/caching.html +23 -0
  195. nautobot/project-static/docs/development/core/controllers.html +23 -0
  196. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +23 -0
  197. nautobot/project-static/docs/development/core/generic-views.html +23 -0
  198. nautobot/project-static/docs/development/core/getting-started.html +23 -0
  199. nautobot/project-static/docs/development/core/homepage.html +23 -0
  200. nautobot/project-static/docs/development/core/index.html +23 -0
  201. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +23 -0
  202. nautobot/project-static/docs/development/core/model-checklist.html +23 -0
  203. nautobot/project-static/docs/development/core/model-features.html +23 -0
  204. nautobot/project-static/docs/development/core/natural-keys.html +23 -0
  205. nautobot/project-static/docs/development/core/navigation-menu.html +23 -0
  206. nautobot/project-static/docs/development/core/release-checklist.html +23 -0
  207. nautobot/project-static/docs/development/core/role-internals.html +23 -0
  208. nautobot/project-static/docs/development/core/settings.html +23 -0
  209. nautobot/project-static/docs/development/core/style-guide.html +23 -0
  210. nautobot/project-static/docs/development/core/templates.html +23 -0
  211. nautobot/project-static/docs/development/core/testing.html +23 -0
  212. nautobot/project-static/docs/development/core/ui-component-framework.html +713 -255
  213. nautobot/project-static/docs/development/core/user-preferences.html +23 -0
  214. nautobot/project-static/docs/development/index.html +23 -0
  215. nautobot/project-static/docs/development/jobs/getting-started.html +23 -0
  216. nautobot/project-static/docs/development/jobs/index.html +23 -0
  217. nautobot/project-static/docs/development/jobs/installation.html +23 -0
  218. nautobot/project-static/docs/development/jobs/job-extensions.html +23 -0
  219. nautobot/project-static/docs/development/jobs/job-logging.html +23 -0
  220. nautobot/project-static/docs/development/jobs/job-patterns.html +23 -0
  221. nautobot/project-static/docs/development/jobs/job-structure.html +23 -0
  222. nautobot/project-static/docs/development/jobs/migration/from-v1.html +23 -0
  223. nautobot/project-static/docs/development/jobs/testing.html +23 -0
  224. nautobot/project-static/docs/index.html +23 -0
  225. nautobot/project-static/docs/media/development/core/ui-component-framework/breadcrumbs-titles-data-flow.png +0 -0
  226. nautobot/project-static/docs/media/power_distribution.png +0 -0
  227. nautobot/project-static/docs/objects.inv +0 -0
  228. nautobot/project-static/docs/overview/application_stack.html +23 -0
  229. nautobot/project-static/docs/overview/design_philosophy.html +23 -0
  230. nautobot/project-static/docs/release-notes/index.html +23 -0
  231. nautobot/project-static/docs/release-notes/version-1.0.html +23 -0
  232. nautobot/project-static/docs/release-notes/version-1.1.html +23 -0
  233. nautobot/project-static/docs/release-notes/version-1.2.html +23 -0
  234. nautobot/project-static/docs/release-notes/version-1.3.html +23 -0
  235. nautobot/project-static/docs/release-notes/version-1.4.html +23 -0
  236. nautobot/project-static/docs/release-notes/version-1.5.html +23 -0
  237. nautobot/project-static/docs/release-notes/version-1.6.html +23 -0
  238. nautobot/project-static/docs/release-notes/version-2.0.html +23 -0
  239. nautobot/project-static/docs/release-notes/version-2.1.html +23 -0
  240. nautobot/project-static/docs/release-notes/version-2.2.html +23 -0
  241. nautobot/project-static/docs/release-notes/version-2.3.html +23 -0
  242. nautobot/project-static/docs/release-notes/version-2.4.html +306 -0
  243. nautobot/project-static/docs/requirements.txt +2 -2
  244. nautobot/project-static/docs/search/search_index.json +1 -1
  245. nautobot/project-static/docs/sitemap.xml +303 -299
  246. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  247. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +23 -0
  248. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +23 -0
  249. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +23 -0
  250. nautobot/project-static/docs/user-guide/administration/configuration/index.html +23 -0
  251. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +23 -0
  252. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +23 -0
  253. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +23 -0
  254. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +23 -0
  255. nautobot/project-static/docs/user-guide/administration/guides/docker.html +23 -0
  256. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +23 -0
  257. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +23 -0
  258. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +23 -0
  259. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +23 -0
  260. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +23 -0
  261. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +23 -0
  262. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +23 -0
  263. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +23 -0
  264. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +23 -0
  265. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +23 -0
  266. nautobot/project-static/docs/user-guide/administration/installation/index.html +23 -0
  267. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +23 -0
  268. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +23 -0
  269. nautobot/project-static/docs/user-guide/administration/installation/services.html +23 -0
  270. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +23 -0
  271. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +23 -0
  272. nautobot/project-static/docs/user-guide/administration/security/index.html +23 -0
  273. nautobot/project-static/docs/user-guide/administration/security/notices.html +23 -0
  274. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +284 -219
  275. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +23 -0
  276. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +23 -0
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +23 -0
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +23 -0
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +23 -0
  280. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +23 -0
  281. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +23 -0
  282. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +23 -0
  283. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +23 -0
  284. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +23 -0
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +23 -0
  286. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +23 -0
  287. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +23 -0
  288. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +23 -0
  289. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +23 -0
  290. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +23 -0
  291. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +23 -0
  292. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +23 -0
  293. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +23 -0
  294. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +23 -0
  295. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +23 -0
  296. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +23 -0
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +23 -0
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +23 -0
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +23 -0
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +23 -0
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +23 -0
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +23 -0
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +23 -0
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +23 -0
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +23 -0
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +23 -0
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +23 -0
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +23 -0
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +23 -0
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +23 -0
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +23 -0
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +23 -0
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +23 -0
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +23 -0
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +23 -0
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +23 -0
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +23 -0
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +23 -0
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +23 -0
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +23 -0
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +23 -0
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +23 -0
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +23 -0
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +23 -0
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +305 -5
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +24 -1
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +23 -0
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +136 -3
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +41 -1
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +40 -0
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +23 -0
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +23 -0
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +23 -0
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +23 -0
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +23 -0
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +23 -0
  337. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +23 -0
  338. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +23 -0
  339. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +23 -0
  340. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +23 -0
  341. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +23 -0
  342. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +23 -0
  343. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +23 -0
  344. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +23 -0
  345. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +23 -0
  346. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +23 -0
  347. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +23 -0
  348. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +23 -0
  349. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +23 -0
  350. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +23 -0
  351. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +23 -0
  352. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +23 -0
  353. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +23 -0
  354. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +23 -0
  355. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +23 -0
  356. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +23 -0
  357. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +23 -0
  358. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +23 -0
  359. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +23 -0
  360. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +23 -0
  361. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +23 -0
  362. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +23 -0
  363. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +23 -0
  364. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +23 -0
  365. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +23 -0
  366. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +23 -0
  367. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +23 -0
  368. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +23 -0
  369. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +23 -0
  370. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +23 -0
  371. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +23 -0
  372. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +23 -0
  373. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +23 -0
  374. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +23 -0
  375. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +23 -0
  376. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +23 -0
  377. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +23 -0
  378. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +23 -0
  379. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +23 -0
  380. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +23 -0
  381. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +23 -0
  382. nautobot/project-static/docs/user-guide/index.html +23 -0
  383. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +23 -0
  384. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +23 -0
  385. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +23 -0
  386. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +23 -0
  387. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +23 -0
  388. nautobot/project-static/docs/user-guide/platform-functionality/events.html +23 -0
  389. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +23 -0
  390. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +23 -0
  391. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +23 -0
  392. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +23 -0
  393. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +23 -0
  394. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +23 -0
  395. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +23 -0
  396. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +24 -1
  397. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +23 -0
  398. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +23 -0
  399. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +23 -0
  400. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +23 -0
  401. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +23 -0
  402. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +23 -0
  403. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +23 -0
  404. nautobot/project-static/docs/user-guide/platform-functionality/note.html +23 -0
  405. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +23 -0
  406. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +23 -0
  407. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +23 -0
  408. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +23 -0
  409. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +23 -0
  410. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +23 -0
  411. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +23 -0
  412. nautobot/project-static/docs/user-guide/platform-functionality/role.html +23 -0
  413. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +23 -0
  414. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +23 -0
  415. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +23 -0
  416. nautobot/project-static/docs/user-guide/platform-functionality/status.html +23 -0
  417. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +23 -0
  418. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +23 -0
  419. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +23 -0
  420. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +23 -0
  421. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +23 -0
  422. nautobot/users/tests/test_api.py +2 -2
  423. nautobot/virtualization/templates/virtualization/virtualmachine.html +2 -252
  424. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +2 -75
  425. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +252 -0
  426. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +75 -0
  427. nautobot/virtualization/urls.py +3 -61
  428. nautobot/virtualization/views.py +48 -72
  429. {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/METADATA +24 -24
  430. {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/RECORD +434 -417
  431. {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/LICENSE.txt +0 -0
  432. {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/NOTICE +0 -0
  433. {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/WHEEL +0 -0
  434. {nautobot-2.4.14.dist-info → nautobot-2.4.16.dist-info}/entry_points.txt +0 -0
@@ -52,6 +52,7 @@ from nautobot.extras.models import (
52
52
  Contact,
53
53
  ContactAssociation,
54
54
  CustomField,
55
+ CustomFieldChoice,
55
56
  CustomLink,
56
57
  DynamicGroup,
57
58
  ExportTemplate,
@@ -638,12 +639,16 @@ class CustomFieldTestCase(
638
639
  ViewTestCases.GetObjectViewTestCase,
639
640
  ViewTestCases.GetObjectChangelogViewTestCase,
640
641
  ViewTestCases.ListObjectsViewTestCase,
642
+ ViewTestCases.BulkEditObjectsViewTestCase,
641
643
  ):
642
644
  model = CustomField
643
645
  slugify_function = staticmethod(slugify_dashes_to_underscores)
644
646
 
645
647
  @classmethod
646
648
  def setUpTestData(cls):
649
+ ipaddress_ct = ContentType.objects.get_for_model(IPAddress)
650
+ prefix_ct = ContentType.objects.get_for_model(Prefix)
651
+ device_ct = ContentType.objects.get_for_model(Device)
647
652
  obj_type = ContentType.objects.get_for_model(Location)
648
653
 
649
654
  custom_fields = [
@@ -674,7 +679,7 @@ class CustomFieldTestCase(
674
679
 
675
680
  for custom_field in custom_fields:
676
681
  custom_field.validated_save()
677
- custom_field.content_types.set([obj_type])
682
+ custom_field.content_types.set([obj_type, device_ct])
678
683
 
679
684
  cls.form_data = {
680
685
  "content_types": [obj_type.pk],
@@ -691,6 +696,17 @@ class CustomFieldTestCase(
691
696
  "custom_field_choices-MAX_NUM_FORMS": "1000",
692
697
  }
693
698
 
699
+ cls.bulk_edit_data = {
700
+ "grouping": "Updated Grouping",
701
+ "description": "Updated description for testing bulk edit.",
702
+ "required": True,
703
+ "filter_logic": "loose",
704
+ "weight": 200,
705
+ "advanced_ui": True,
706
+ "add_content_types": [ipaddress_ct.pk, prefix_ct.pk],
707
+ "remove_content_types": [device_ct.pk],
708
+ }
709
+
694
710
  def test_create_object_without_permission(self):
695
711
  # Can't have two CustomFields with the same "key"
696
712
  self.form_data = self.form_data.copy()
@@ -709,6 +725,161 @@ class CustomFieldTestCase(
709
725
  self.form_data["key"] = "custom_field_boolean_2"
710
726
  super().test_create_object_with_constrained_permission()
711
727
 
728
+ def test_create_custom_field_with_choices(self):
729
+ """Ensure a select-type CustomField can be created with multiple valid choices.."""
730
+ self.add_permissions("extras.add_customfield", "extras.view_customfield")
731
+
732
+ content_type = ContentType.objects.get_for_model(Location)
733
+
734
+ form_data = {
735
+ "content_types": [content_type.pk],
736
+ "type": CustomFieldTypeChoices.TYPE_SELECT,
737
+ "key": "select_with_choices",
738
+ "label": "Select Field with Choices",
739
+ "default": "",
740
+ "filter_logic": "loose",
741
+ "weight": 100,
742
+ "custom_field_choices-TOTAL_FORMS": "2",
743
+ "custom_field_choices-INITIAL_FORMS": "0",
744
+ "custom_field_choices-MIN_NUM_FORMS": "0",
745
+ "custom_field_choices-MAX_NUM_FORMS": "1000",
746
+ "custom_field_choices-0-value": "Option A",
747
+ "custom_field_choices-0-weight": "100",
748
+ "custom_field_choices-1-value": "Option B",
749
+ "custom_field_choices-1-weight": "200",
750
+ }
751
+
752
+ response = self.client.post(reverse("extras:customfield_add"), data=form_data, follow=True)
753
+
754
+ self.assertEqual(response.status_code, 200)
755
+ self.assertTrue(CustomField.objects.filter(key="select_with_choices").exists())
756
+
757
+ field = CustomField.objects.get(key="select_with_choices")
758
+ self.assertEqual(field.custom_field_choices.count(), 2)
759
+ self.assertSetEqual(
760
+ set(field.custom_field_choices.values_list("value", flat=True)),
761
+ {"Option A", "Option B"},
762
+ )
763
+
764
+ def test_update_select_custom_field_add_choice(self):
765
+ """Test that submitting the edit form with both existing and new choices
766
+ results in the new choice being saved correctly."""
767
+ self.add_permissions("extras.change_customfield", "extras.view_customfield")
768
+
769
+ content_type = ContentType.objects.get_for_model(Location)
770
+ field = CustomField.objects.create(
771
+ type=CustomFieldTypeChoices.TYPE_SELECT,
772
+ label="Editable Select Field",
773
+ key="editable_select_field",
774
+ )
775
+ field.content_types.set([content_type])
776
+
777
+ # Added initial choice
778
+ initial_choice = CustomFieldChoice.objects.create(
779
+ custom_field=field,
780
+ value="Initial Option",
781
+ weight=100,
782
+ )
783
+
784
+ url = reverse("extras:customfield_edit", args=[field.pk])
785
+ form_data = {
786
+ "content_types": [content_type.pk],
787
+ "type": field.type,
788
+ "key": field.key,
789
+ "label": field.label,
790
+ "default": "",
791
+ "filter_logic": "loose",
792
+ "weight": 100,
793
+ "custom_field_choices-TOTAL_FORMS": "2",
794
+ "custom_field_choices-INITIAL_FORMS": "1",
795
+ "custom_field_choices-MIN_NUM_FORMS": "0",
796
+ "custom_field_choices-MAX_NUM_FORMS": "1000",
797
+ "custom_field_choices-0-id": initial_choice.pk,
798
+ "custom_field_choices-0-value": "Initial Option",
799
+ "custom_field_choices-0-weight": "100",
800
+ "custom_field_choices-1-value": "New Option",
801
+ "custom_field_choices-1-weight": "200",
802
+ }
803
+
804
+ response = self.client.post(url, data=form_data, follow=True)
805
+ self.assertEqual(response.status_code, 200)
806
+ self.assertEqual(field.custom_field_choices.count(), 2)
807
+ self.assertTrue(field.custom_field_choices.filter(value="New Option").exists())
808
+
809
+ def test_update_select_custom_field_remove_choice(self):
810
+ """Test removing a choice from a select field."""
811
+ self.add_permissions("extras.change_customfield", "extras.view_customfield")
812
+
813
+ content_type = ContentType.objects.get_for_model(Location)
814
+ field = CustomField.objects.create(
815
+ type=CustomFieldTypeChoices.TYPE_SELECT,
816
+ label="Deletable Select Field",
817
+ key="deletable_select_field",
818
+ )
819
+ field.content_types.set([content_type])
820
+
821
+ choice = CustomFieldChoice.objects.create(
822
+ custom_field=field,
823
+ value="Choice To Delete",
824
+ weight=100,
825
+ )
826
+
827
+ url = reverse("extras:customfield_edit", args=[field.pk])
828
+ form_data = {
829
+ "content_types": [content_type.pk],
830
+ "type": field.type,
831
+ "key": field.key,
832
+ "label": field.label,
833
+ "default": "",
834
+ "filter_logic": "loose",
835
+ "weight": 100,
836
+ "custom_field_choices-TOTAL_FORMS": "1",
837
+ "custom_field_choices-INITIAL_FORMS": "1",
838
+ "custom_field_choices-MIN_NUM_FORMS": "0",
839
+ "custom_field_choices-MAX_NUM_FORMS": "1000",
840
+ "custom_field_choices-0-id": choice.pk,
841
+ "custom_field_choices-0-value": choice.value,
842
+ "custom_field_choices-0-weight": choice.weight,
843
+ "custom_field_choices-0-DELETE": "on",
844
+ }
845
+
846
+ response = self.client.post(url, data=form_data, follow=True)
847
+ self.assertEqual(response.status_code, 200)
848
+ self.assertEqual(field.custom_field_choices.count(), 0)
849
+
850
+ def test_create_custom_field_with_invalid_choice_data(self):
851
+ """Ensure invalid choice formset blocks saving."""
852
+ self.add_permissions("extras.add_customfield", "extras.view_customfield")
853
+
854
+ content_type = ContentType.objects.get_for_model(Location)
855
+
856
+ form_data = {
857
+ "content_types": [content_type.pk],
858
+ "type": CustomFieldTypeChoices.TYPE_SELECT,
859
+ "key": "invalid_choice_field",
860
+ "label": "Field with Invalid Choice",
861
+ "default": "",
862
+ "filter_logic": "loose",
863
+ "weight": 100,
864
+ "custom_field_choices-TOTAL_FORMS": "1",
865
+ "custom_field_choices-INITIAL_FORMS": "0",
866
+ "custom_field_choices-MIN_NUM_FORMS": "0",
867
+ "custom_field_choices-MAX_NUM_FORMS": "1000",
868
+ # Invalid: missing weight, empty value
869
+ "custom_field_choices-0-value": "",
870
+ }
871
+
872
+ response = self.client.post(reverse("extras:customfield_add"), data=form_data)
873
+
874
+ self.assertEqual(response.status_code, 200)
875
+ self.assertFalse(CustomField.objects.filter(key="invalid_choice_field").exists())
876
+ self.assertFormsetError(
877
+ response.context["choices"], form_index=0, field="value", errors=["This field is required."]
878
+ )
879
+ self.assertFormsetError(
880
+ response.context["choices"], form_index=0, field="weight", errors=["This field is required."]
881
+ )
882
+
712
883
 
713
884
  class CustomLinkRenderingTestCase(TestCase):
714
885
  """Tests for the inclusion of CustomLinks, distinct from tests of the CustomLink views themselves."""
nautobot/extras/urls.py CHANGED
@@ -3,11 +3,9 @@ from django.urls import path
3
3
  from nautobot.core.views.routers import NautobotUIViewSetRouter
4
4
  from nautobot.extras import views
5
5
  from nautobot.extras.models import (
6
- CustomField,
7
6
  DynamicGroup,
8
7
  GitRepository,
9
8
  Job,
10
- Note,
11
9
  Relationship,
12
10
  )
13
11
 
@@ -19,6 +17,7 @@ router.register("config-context-schemas", views.ConfigContextSchemaUIViewSet)
19
17
  router.register("config-contexts", views.ConfigContextUIViewSet)
20
18
  router.register("contacts", views.ContactUIViewSet)
21
19
  router.register("contact-associations", views.ContactAssociationUIViewSet)
20
+ router.register("custom-fields", views.CustomFieldUIViewSet)
22
21
  router.register("custom-links", views.CustomLinkUIViewSet)
23
22
  router.register("export-templates", views.ExportTemplateUIViewSet)
24
23
  router.register("external-integrations", views.ExternalIntegrationUIViewSet)
@@ -26,7 +25,9 @@ router.register("graphql-queries", views.GraphQLQueryUIViewSet)
26
25
  router.register("job-buttons", views.JobButtonUIViewSet)
27
26
  router.register("job-hooks", views.JobHookUIViewSet)
28
27
  router.register("job-queues", views.JobQueueUIViewSet)
28
+ router.register("job-results", views.JobResultUIViewSet)
29
29
  router.register("metadata-types", views.MetadataTypeUIViewSet)
30
+ router.register("notes", views.NoteUIViewSet)
30
31
  router.register("object-metadata", views.ObjectMetadataUIViewSet)
31
32
  router.register("relationship-associations", views.RelationshipAssociationUIViewSet)
32
33
  router.register("relationships", views.RelationshipUIViewSet)
@@ -58,37 +59,6 @@ urlpatterns = [
58
59
  views.ObjectAssignContactOrTeamView.as_view(),
59
60
  name="object_contact_team_assign",
60
61
  ),
61
- # Custom fields
62
- path("custom-fields/", views.CustomFieldListView.as_view(), name="customfield_list"),
63
- path("custom-fields/add/", views.CustomFieldEditView.as_view(), name="customfield_add"),
64
- path(
65
- "custom-fields/delete/",
66
- views.CustomFieldBulkDeleteView.as_view(),
67
- name="customfield_bulk_delete",
68
- ),
69
- path("custom-fields/<uuid:pk>/", views.CustomFieldView.as_view(), name="customfield"),
70
- path(
71
- "custom-fields/<uuid:pk>/edit/",
72
- views.CustomFieldEditView.as_view(),
73
- name="customfield_edit",
74
- ),
75
- path(
76
- "custom-fields/<uuid:pk>/delete/",
77
- views.CustomFieldDeleteView.as_view(),
78
- name="customfield_delete",
79
- ),
80
- path(
81
- "custom-fields/<uuid:pk>/changelog/",
82
- views.ObjectChangeLogView.as_view(),
83
- name="customfield_changelog",
84
- kwargs={"model": CustomField},
85
- ),
86
- path(
87
- "custom-fields/<uuid:pk>/notes/",
88
- views.ObjectNotesView.as_view(),
89
- name="customfield_notes",
90
- kwargs={"model": CustomField},
91
- ),
92
62
  # Dynamic Groups
93
63
  path("dynamic-groups/", views.DynamicGroupListView.as_view(), name="dynamicgroup_list"),
94
64
  path("dynamic-groups/add/", views.DynamicGroupEditView.as_view(), name="dynamicgroup_add"),
@@ -235,32 +205,6 @@ urlpatterns = [
235
205
  path("jobs/<str:class_path>/run/", views.JobRunView.as_view(), name="job_run_by_class_path"),
236
206
  path("jobs/edit/", views.JobBulkEditView.as_view(), name="job_bulk_edit"),
237
207
  path("jobs/delete/", views.JobBulkDeleteView.as_view(), name="job_bulk_delete"),
238
- # Generic job results
239
- path("job-results/", views.JobResultListView.as_view(), name="jobresult_list"),
240
- path("job-results/<uuid:pk>/", views.JobResultView.as_view(), name="jobresult"),
241
- path("job-results/<uuid:pk>/log-table/", views.JobLogEntryTableView.as_view(), name="jobresult_log-table"),
242
- path(
243
- "job-results/delete/",
244
- views.JobResultBulkDeleteView.as_view(),
245
- name="jobresult_bulk_delete",
246
- ),
247
- path(
248
- "job-results/<uuid:pk>/delete/",
249
- views.JobResultDeleteView.as_view(),
250
- name="jobresult_delete",
251
- ),
252
- # Notes
253
- path("notes/", views.NoteListView.as_view(), name="note_list"),
254
- path("notes/add/", views.NoteEditView.as_view(), name="note_add"),
255
- path("notes/<uuid:pk>/", views.NoteView.as_view(), name="note"),
256
- path(
257
- "notes/<uuid:pk>/changelog/",
258
- views.ObjectChangeLogView.as_view(),
259
- name="note_changelog",
260
- kwargs={"model": Note},
261
- ),
262
- path("notes/<uuid:pk>/edit/", views.NoteEditView.as_view(), name="note_edit"),
263
- path("notes/<uuid:pk>/delete/", views.NoteDeleteView.as_view(), name="note_delete"),
264
208
  # Custom relationships
265
209
  path(
266
210
  "relationships/<uuid:pk>/changelog/",
nautobot/extras/utils.py CHANGED
@@ -492,7 +492,7 @@ def task_queues_as_choices(task_queues):
492
492
  worker_count = celery_queues.get(settings.CELERY_TASK_DEFAULT_QUEUE, 0)
493
493
  else:
494
494
  worker_count = celery_queues.get(queue, 0)
495
- description = f"{queue if queue else 'default queue'} ({worker_count} worker{'s'[:worker_count^1]})"
495
+ description = f"{queue if queue else 'default queue'} ({worker_count} worker{'s'[: worker_count ^ 1]})"
496
496
  choices.append((queue, description))
497
497
  return choices
498
498
 
nautobot/extras/views.py CHANGED
@@ -125,7 +125,6 @@ from .models import (
125
125
  ScheduledJob,
126
126
  Secret,
127
127
  SecretsGroup,
128
- SecretsGroupAssociation,
129
128
  StaticGroupAssociation,
130
129
  Status,
131
130
  Tag,
@@ -566,132 +565,45 @@ class ObjectAssignContactOrTeamView(generic.ObjectEditView):
566
565
  #
567
566
 
568
567
 
569
- class CustomFieldListView(generic.ObjectListView):
568
+ class CustomFieldUIViewSet(NautobotUIViewSet):
569
+ bulk_update_form_class = forms.CustomFieldBulkEditForm
570
570
  queryset = CustomField.objects.all()
571
- table = tables.CustomFieldTable
572
- filterset = filters.CustomFieldFilterSet
573
- filterset_form = forms.CustomFieldFilterForm
571
+ serializer_class = serializers.CustomFieldSerializer
572
+ filterset_class = filters.CustomFieldFilterSet
573
+ filterset_form_class = forms.CustomFieldFilterForm
574
+ form_class = forms.CustomFieldForm
575
+ table_class = tables.CustomFieldTable
576
+ template_name = "extras/customfield_update.html"
574
577
  action_buttons = ("add",)
575
578
 
576
-
577
- class CustomFieldView(generic.ObjectView):
578
- queryset = CustomField.objects.all()
579
-
580
-
581
- class CustomFieldEditView(generic.ObjectEditView):
582
- queryset = CustomField.objects.all()
583
- model_form = forms.CustomFieldForm
584
- template_name = "extras/customfield_edit.html"
585
-
586
579
  def get_extra_context(self, request, instance):
587
- ctx = super().get_extra_context(request, instance)
588
-
589
- if request.POST:
590
- ctx["choices"] = forms.CustomFieldChoiceFormSet(data=request.POST, instance=instance)
591
- else:
592
- ctx["choices"] = forms.CustomFieldChoiceFormSet(instance=instance)
593
-
594
- return ctx
595
-
596
- def post(self, request, *args, **kwargs):
597
- obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
598
- form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
599
- restrict_form_fields(form, request.user)
600
-
601
- if form.is_valid():
602
- logger.debug("Form validation was successful")
603
-
604
- try:
605
- with transaction.atomic():
606
- object_created = not form.instance.present_in_database
607
- obj = form.save()
608
-
609
- # Check that the new object conforms with any assigned object-level permissions
610
- self.queryset.get(pk=obj.pk)
611
-
612
- # ---> BEGIN difference from ObjectEditView.post()
613
- # Process the formsets for choices
614
- ctx = self.get_extra_context(request, obj)
615
- choices = ctx["choices"]
616
- if choices.is_valid():
617
- choices.save()
618
- else:
619
- raise RuntimeError(choices.errors)
620
- # <--- END difference from ObjectEditView.post()
621
- verb = "Created" if object_created else "Modified"
622
- msg = f"{verb} {self.queryset.model._meta.verbose_name}"
623
- logger.info(f"{msg} {obj} (PK: {obj.pk})")
624
- try:
625
- msg = format_html('{} <a href="{}">{}</a>', msg, obj.get_absolute_url(), obj)
626
- except AttributeError:
627
- msg = format_html("{} {}", msg, obj)
628
- messages.success(request, msg)
629
-
630
- if "_addanother" in request.POST:
631
- # If the object has clone_fields, pre-populate a new instance of the form
632
- if hasattr(obj, "clone_fields"):
633
- url = f"{request.path}?{prepare_cloned_fields(obj)}"
634
- return redirect(url)
580
+ context = super().get_extra_context(request, instance)
635
581
 
636
- return redirect(request.get_full_path())
582
+ if self.action in ("create", "update"):
583
+ if request.POST:
584
+ context["choices"] = forms.CustomFieldChoiceFormSet(data=request.POST, instance=instance)
585
+ else:
586
+ context["choices"] = forms.CustomFieldChoiceFormSet(instance=instance)
637
587
 
638
- return_url = form.cleaned_data.get("return_url")
639
- if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
640
- return redirect(iri_to_uri(return_url))
641
- else:
642
- return redirect(self.get_return_url(request, obj))
588
+ return context
643
589
 
644
- except ObjectDoesNotExist:
645
- msg = "Object save failed due to object-level permissions violation"
646
- logger.debug(msg)
647
- form.add_error(None, msg)
648
- # ---> BEGIN difference from ObjectEditView.post()
649
- except RuntimeError:
650
- msg = "Errors encountered when saving custom field choices. See below."
651
- logger.debug(msg)
652
- form.add_error(None, msg)
653
- except ProtectedError as err:
654
- # e.g. Trying to delete a choice that is in use.
655
- err_msg = err.args[0]
656
- protected_obj = err.protected_objects[0]
657
- msg = f"{protected_obj.value}: {err_msg} Please cancel this edit and start again."
658
- logger.debug(msg)
659
- form.add_error(None, msg)
660
- # <--- END difference from ObjectEditView.post()
590
+ def form_save(self, form, **kwargs):
591
+ obj = super().form_save(form, **kwargs)
661
592
 
593
+ # Process the formset for choices
594
+ ctx = self.get_extra_context(self.request, obj)
595
+ choices = ctx["choices"]
596
+ if choices.is_valid():
597
+ choices.save()
662
598
  else:
663
- logger.debug("Form validation failed")
664
-
665
- return render(
666
- request,
667
- self.template_name,
668
- {
669
- "obj": obj,
670
- "obj_type": self.queryset.model._meta.verbose_name,
671
- "form": form,
672
- "return_url": self.get_return_url(request, obj),
673
- "editing": obj.present_in_database,
674
- **self.get_extra_context(request, obj),
675
- },
676
- )
677
-
678
-
679
- class CustomFieldDeleteView(generic.ObjectDeleteView):
680
- queryset = CustomField.objects.all()
681
-
599
+ raise ValidationError(choices.errors)
682
600
 
683
- class CustomFieldBulkDeleteView(generic.BulkDeleteView):
684
- queryset = CustomField.objects.all()
685
- table = tables.CustomFieldTable
686
- filterset = filters.CustomFieldFilterSet
687
- form = forms.CustomFieldBulkDeleteForm
601
+ return obj
688
602
 
689
603
 
690
604
  #
691
605
  # Custom Links
692
606
  #
693
-
694
-
695
607
  class CustomLinkUIViewSet(NautobotUIViewSet):
696
608
  bulk_update_form_class = forms.CustomLinkBulkEditForm
697
609
  filterset_class = filters.CustomLinkFilterSet
@@ -2140,59 +2052,57 @@ class JobHookUIViewSet(NautobotUIViewSet):
2140
2052
  #
2141
2053
 
2142
2054
 
2143
- class JobResultListView(generic.ObjectListView):
2144
- """
2145
- List JobResults
2146
- """
2147
-
2148
- queryset = JobResult.objects.defer("result").select_related("job_model", "user")
2149
- filterset = filters.JobResultFilterSet
2150
- filterset_form = forms.JobResultFilterForm
2151
- table = tables.JobResultTable
2152
- action_buttons = ()
2153
-
2154
-
2155
- class JobResultDeleteView(generic.ObjectDeleteView):
2055
+ class JobResultUIViewSet(
2056
+ ObjectDetailViewMixin,
2057
+ ObjectListViewMixin,
2058
+ ObjectDestroyViewMixin,
2059
+ ObjectBulkDestroyViewMixin,
2060
+ ):
2061
+ filterset_class = filters.JobResultFilterSet
2062
+ filterset_form_class = forms.JobResultFilterForm
2063
+ serializer_class = serializers.JobResultSerializer
2064
+ table_class = tables.JobResultTable
2156
2065
  queryset = JobResult.objects.all()
2157
-
2158
-
2159
- class JobResultBulkDeleteView(generic.BulkDeleteView):
2160
- queryset = JobResult.objects.defer("result").select_related("job_model", "user")
2161
- table = tables.JobResultTable
2162
- filterset = filters.JobResultFilterSet
2163
-
2164
-
2165
- class JobResultView(generic.ObjectView):
2166
- """
2167
- Display a JobResult and its Job data.
2168
- """
2169
-
2170
- queryset = JobResult.objects.prefetch_related("job_model", "user")
2171
- template_name = "extras/jobresult.html"
2066
+ action_buttons = ()
2172
2067
 
2173
2068
  def get_extra_context(self, request, instance):
2174
- associated_record = None
2175
- job_class = None
2176
- if instance.job_model is not None:
2177
- job_class = instance.job_model.job_class
2069
+ context = super().get_extra_context(request, instance)
2070
+ if self.action == "retrieve":
2071
+ job_class = None
2072
+ if instance and instance.job_model:
2073
+ job_class = instance.job_model.job_class
2074
+
2075
+ context.update(
2076
+ {
2077
+ "job": job_class,
2078
+ "associated_record": None,
2079
+ "result": instance,
2080
+ }
2081
+ )
2178
2082
 
2179
- return {
2180
- "job": job_class,
2181
- "associated_record": associated_record,
2182
- "result": instance,
2183
- **super().get_extra_context(request, instance),
2184
- }
2083
+ return context
2185
2084
 
2085
+ def get_queryset(self):
2086
+ queryset = super().get_queryset().select_related("job_model", "user")
2186
2087
 
2187
- class JobLogEntryTableView(generic.GenericView):
2188
- """
2189
- Display a table of `JobLogEntry` objects for a given `JobResult` instance.
2190
- """
2088
+ if not self.detail:
2089
+ queryset = queryset.defer("result", "task_args", "task_kwargs", "celery_kwargs", "traceback", "meta")
2191
2090
 
2192
- queryset = JobResult.objects.all()
2091
+ return queryset
2092
+
2093
+ @action(
2094
+ detail=True,
2095
+ url_path="log-table",
2096
+ url_name="log-table",
2097
+ custom_view_base_action="view",
2098
+ )
2099
+ def log_table(self, request, pk=None):
2100
+ """
2101
+ Custom action to return a rendered JobLogEntry table for a JobResult.
2102
+ """
2193
2103
 
2194
- def get(self, request, pk=None):
2195
2104
  instance = get_object_or_404(self.queryset.restrict(request.user, "view"), pk=pk)
2105
+
2196
2106
  filter_q = request.GET.get("q")
2197
2107
  if filter_q:
2198
2108
  queryset = instance.job_log_entries.filter(
@@ -2200,14 +2110,15 @@ class JobLogEntryTableView(generic.GenericView):
2200
2110
  )
2201
2111
  else:
2202
2112
  queryset = instance.job_log_entries.all()
2113
+
2203
2114
  log_table = tables.JobLogEntryTable(data=queryset, user=request.user)
2204
2115
  paginate = {
2205
2116
  "paginator_class": EnhancedPaginator,
2206
2117
  "per_page": get_paginate_count(request),
2207
2118
  }
2208
2119
  RequestConfig(request, paginate).configure(log_table)
2209
- table = log_table.as_html(request)
2210
- return HttpResponse(table)
2120
+
2121
+ return HttpResponse(log_table.as_html(request))
2211
2122
 
2212
2123
 
2213
2124
  #
@@ -2423,35 +2334,22 @@ class ObjectMetadataUIViewSet(
2423
2334
  #
2424
2335
 
2425
2336
 
2426
- class NoteView(generic.ObjectView):
2427
- queryset = Note.objects.all()
2428
-
2429
-
2430
- class NoteListView(generic.ObjectListView):
2431
- """
2432
- List Notes
2433
- """
2434
-
2337
+ class NoteUIViewSet(
2338
+ ObjectChangeLogViewMixin, ObjectDestroyViewMixin, ObjectDetailViewMixin, ObjectEditViewMixin, ObjectListViewMixin
2339
+ ):
2340
+ filterset_class = filters.NoteFilterSet
2341
+ filterset_form_class = forms.NoteFilterForm
2342
+ form_class = forms.NoteForm
2435
2343
  queryset = Note.objects.all()
2436
- filterset = filters.NoteFilterSet
2437
- filterset_form = forms.NoteFilterForm
2438
- table = tables.NoteTable
2344
+ serializer_class = serializers.NoteSerializer
2345
+ table_class = tables.NoteTable
2439
2346
  action_buttons = ()
2440
2347
 
2441
-
2442
- class NoteEditView(generic.ObjectEditView):
2443
- queryset = Note.objects.all()
2444
- model_form = forms.NoteForm
2445
-
2446
2348
  def alter_obj(self, obj, request, url_args, url_kwargs):
2447
2349
  obj.user = request.user
2448
2350
  return obj
2449
2351
 
2450
2352
 
2451
- class NoteDeleteView(generic.ObjectDeleteView):
2452
- queryset = Note.objects.all()
2453
-
2454
-
2455
2353
  class ObjectNotesView(generic.GenericView):
2456
2354
  """
2457
2355
  Present a list of notes associated to a particular object.
@@ -2744,13 +2642,29 @@ class SecretsGroupUIViewSet(NautobotUIViewSet):
2744
2642
  form_class = forms.SecretsGroupForm
2745
2643
  serializer_class = serializers.SecretsGroupSerializer
2746
2644
  table_class = tables.SecretsGroupTable
2747
- template_name = "extras/secretsgroup_update.html"
2748
2645
  queryset = SecretsGroup.objects.all()
2749
2646
 
2647
+ object_detail_content = ObjectDetailContent(
2648
+ panels=(
2649
+ object_detail.ObjectFieldsPanel(
2650
+ label="Secrets Group Details",
2651
+ fields=["description"],
2652
+ section=SectionChoices.LEFT_HALF,
2653
+ weight=100,
2654
+ ),
2655
+ object_detail.ObjectsTablePanel(
2656
+ table_class=tables.SecretsGroupAssociationTable,
2657
+ table_filter="secrets_group",
2658
+ related_field_name="secrets_group",
2659
+ table_title="Secrets",
2660
+ section=SectionChoices.LEFT_HALF,
2661
+ weight=200,
2662
+ ),
2663
+ )
2664
+ )
2665
+
2750
2666
  def get_extra_context(self, request, instance=None):
2751
2667
  context = super().get_extra_context(request, instance)
2752
- if self.action == "retrieve" and instance:
2753
- context["secrets_group_associations"] = SecretsGroupAssociation.objects.filter(secrets_group=instance)
2754
2668
  if self.action in ("create", "update"):
2755
2669
  if request.method == "POST":
2756
2670
  context["secrets"] = forms.SecretsGroupAssociationFormSet(data=request.POST, instance=instance)