nautobot 2.4.14__py3-none-any.whl → 2.4.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (433) hide show
  1. nautobot/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 +9 -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/templatetags/custom_links.py +2 -2
  92. nautobot/extras/templatetags/job_buttons.py +1 -1
  93. nautobot/extras/templatetags/plugins.py +1 -1
  94. nautobot/extras/tests/integration/test_computedfields.py +2 -2
  95. nautobot/extras/tests/integration/test_customfields.py +14 -11
  96. nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
  97. nautobot/extras/tests/integration/test_notes.py +1 -1
  98. nautobot/extras/tests/integration/test_plugins.py +6 -6
  99. nautobot/extras/tests/integration/test_relationships.py +2 -2
  100. nautobot/extras/tests/test_filters.py +9 -0
  101. nautobot/extras/tests/test_forms.py +2 -2
  102. nautobot/extras/tests/test_plugins.py +2 -3
  103. nautobot/extras/tests/test_relationships.py +7 -7
  104. nautobot/extras/tests/test_views.py +172 -1
  105. nautobot/extras/urls.py +3 -59
  106. nautobot/extras/utils.py +1 -1
  107. nautobot/extras/views.py +77 -178
  108. nautobot/ipam/tables.py +8 -15
  109. nautobot/ipam/tests/migration/test_migrations.py +8 -8
  110. nautobot/ipam/tests/test_api.py +2 -2
  111. nautobot/ipam/tests/test_models.py +1 -1
  112. nautobot/project-static/docs/404.html +23 -0
  113. nautobot/project-static/docs/apps/index.html +23 -0
  114. nautobot/project-static/docs/apps/nautobot-apps.html +23 -0
  115. nautobot/project-static/docs/assets/_mkdocstrings.css +44 -6
  116. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +28 -0
  117. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +25 -0
  118. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +128 -20
  119. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +37 -4
  120. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +39 -6
  121. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +25 -0
  122. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +24 -0
  123. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +32 -5
  124. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +41 -8
  125. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +39 -7
  126. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +43 -10
  127. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +74 -59
  128. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +143 -28
  129. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +43 -12
  130. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +135 -53
  131. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +229 -36
  132. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +27 -1
  133. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +30 -1
  134. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +162 -18
  135. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +258 -51
  136. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +5987 -2620
  137. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +25 -0
  138. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +154 -55
  139. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +150 -35
  140. nautobot/project-static/docs/development/apps/api/configuration-view.html +23 -0
  141. nautobot/project-static/docs/development/apps/api/database-backend-config.html +23 -0
  142. nautobot/project-static/docs/development/apps/api/models/django-admin.html +23 -0
  143. nautobot/project-static/docs/development/apps/api/models/global-search.html +23 -0
  144. nautobot/project-static/docs/development/apps/api/models/graphql.html +23 -0
  145. nautobot/project-static/docs/development/apps/api/models/index.html +23 -0
  146. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +23 -0
  147. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +23 -0
  148. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +23 -0
  149. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +23 -0
  150. nautobot/project-static/docs/development/apps/api/platform-features/index.html +23 -0
  151. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +23 -0
  152. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +23 -0
  153. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +23 -0
  154. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +23 -0
  155. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +23 -0
  156. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +23 -0
  157. nautobot/project-static/docs/development/apps/api/prometheus.html +23 -0
  158. nautobot/project-static/docs/development/apps/api/setup.html +23 -0
  159. nautobot/project-static/docs/development/apps/api/testing.html +23 -0
  160. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +23 -0
  161. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +23 -0
  162. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +23 -0
  163. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +23 -0
  164. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +23 -0
  165. nautobot/project-static/docs/development/apps/api/views/base-template.html +23 -0
  166. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +23 -0
  167. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +23 -0
  168. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +23 -0
  169. nautobot/project-static/docs/development/apps/api/views/index.html +23 -0
  170. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +23 -0
  171. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +31 -2
  172. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +23 -0
  173. nautobot/project-static/docs/development/apps/api/views/notes.html +23 -0
  174. nautobot/project-static/docs/development/apps/api/views/rest-api.html +23 -0
  175. nautobot/project-static/docs/development/apps/api/views/urls.html +23 -0
  176. nautobot/project-static/docs/development/apps/index.html +23 -0
  177. nautobot/project-static/docs/development/apps/migration/code-updates.html +23 -0
  178. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +23 -0
  179. nautobot/project-static/docs/development/apps/migration/from-v1.html +23 -0
  180. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +23 -0
  181. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +23 -0
  182. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +23 -0
  183. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +23 -0
  184. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +26 -3
  185. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +10544 -0
  186. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +23 -0
  187. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +23 -0
  188. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +23 -0
  189. nautobot/project-static/docs/development/apps/porting-from-netbox.html +26 -3
  190. nautobot/project-static/docs/development/core/application-registry.html +23 -0
  191. nautobot/project-static/docs/development/core/best-practices.html +23 -0
  192. nautobot/project-static/docs/development/core/bootstrap-ui.html +23 -0
  193. nautobot/project-static/docs/development/core/caching.html +23 -0
  194. nautobot/project-static/docs/development/core/controllers.html +23 -0
  195. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +23 -0
  196. nautobot/project-static/docs/development/core/generic-views.html +23 -0
  197. nautobot/project-static/docs/development/core/getting-started.html +23 -0
  198. nautobot/project-static/docs/development/core/homepage.html +23 -0
  199. nautobot/project-static/docs/development/core/index.html +23 -0
  200. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +23 -0
  201. nautobot/project-static/docs/development/core/model-checklist.html +23 -0
  202. nautobot/project-static/docs/development/core/model-features.html +23 -0
  203. nautobot/project-static/docs/development/core/natural-keys.html +23 -0
  204. nautobot/project-static/docs/development/core/navigation-menu.html +23 -0
  205. nautobot/project-static/docs/development/core/release-checklist.html +23 -0
  206. nautobot/project-static/docs/development/core/role-internals.html +23 -0
  207. nautobot/project-static/docs/development/core/settings.html +23 -0
  208. nautobot/project-static/docs/development/core/style-guide.html +23 -0
  209. nautobot/project-static/docs/development/core/templates.html +23 -0
  210. nautobot/project-static/docs/development/core/testing.html +23 -0
  211. nautobot/project-static/docs/development/core/ui-component-framework.html +713 -255
  212. nautobot/project-static/docs/development/core/user-preferences.html +23 -0
  213. nautobot/project-static/docs/development/index.html +23 -0
  214. nautobot/project-static/docs/development/jobs/getting-started.html +23 -0
  215. nautobot/project-static/docs/development/jobs/index.html +23 -0
  216. nautobot/project-static/docs/development/jobs/installation.html +23 -0
  217. nautobot/project-static/docs/development/jobs/job-extensions.html +23 -0
  218. nautobot/project-static/docs/development/jobs/job-logging.html +23 -0
  219. nautobot/project-static/docs/development/jobs/job-patterns.html +23 -0
  220. nautobot/project-static/docs/development/jobs/job-structure.html +23 -0
  221. nautobot/project-static/docs/development/jobs/migration/from-v1.html +23 -0
  222. nautobot/project-static/docs/development/jobs/testing.html +23 -0
  223. nautobot/project-static/docs/index.html +23 -0
  224. nautobot/project-static/docs/media/development/core/ui-component-framework/breadcrumbs-titles-data-flow.png +0 -0
  225. nautobot/project-static/docs/media/power_distribution.png +0 -0
  226. nautobot/project-static/docs/objects.inv +0 -0
  227. nautobot/project-static/docs/overview/application_stack.html +23 -0
  228. nautobot/project-static/docs/overview/design_philosophy.html +23 -0
  229. nautobot/project-static/docs/release-notes/index.html +23 -0
  230. nautobot/project-static/docs/release-notes/version-1.0.html +23 -0
  231. nautobot/project-static/docs/release-notes/version-1.1.html +23 -0
  232. nautobot/project-static/docs/release-notes/version-1.2.html +23 -0
  233. nautobot/project-static/docs/release-notes/version-1.3.html +23 -0
  234. nautobot/project-static/docs/release-notes/version-1.4.html +23 -0
  235. nautobot/project-static/docs/release-notes/version-1.5.html +23 -0
  236. nautobot/project-static/docs/release-notes/version-1.6.html +23 -0
  237. nautobot/project-static/docs/release-notes/version-2.0.html +23 -0
  238. nautobot/project-static/docs/release-notes/version-2.1.html +23 -0
  239. nautobot/project-static/docs/release-notes/version-2.2.html +23 -0
  240. nautobot/project-static/docs/release-notes/version-2.3.html +23 -0
  241. nautobot/project-static/docs/release-notes/version-2.4.html +231 -0
  242. nautobot/project-static/docs/requirements.txt +2 -2
  243. nautobot/project-static/docs/search/search_index.json +1 -1
  244. nautobot/project-static/docs/sitemap.xml +303 -299
  245. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  246. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +23 -0
  247. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +23 -0
  248. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +23 -0
  249. nautobot/project-static/docs/user-guide/administration/configuration/index.html +23 -0
  250. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +23 -0
  251. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +23 -0
  252. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +23 -0
  253. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +23 -0
  254. nautobot/project-static/docs/user-guide/administration/guides/docker.html +23 -0
  255. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +23 -0
  256. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +23 -0
  257. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +23 -0
  258. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +23 -0
  259. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +23 -0
  260. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +23 -0
  261. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +23 -0
  262. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +23 -0
  263. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +23 -0
  264. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +23 -0
  265. nautobot/project-static/docs/user-guide/administration/installation/index.html +23 -0
  266. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +23 -0
  267. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +23 -0
  268. nautobot/project-static/docs/user-guide/administration/installation/services.html +23 -0
  269. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +23 -0
  270. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +23 -0
  271. nautobot/project-static/docs/user-guide/administration/security/index.html +23 -0
  272. nautobot/project-static/docs/user-guide/administration/security/notices.html +23 -0
  273. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +284 -219
  274. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +23 -0
  275. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +23 -0
  276. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +23 -0
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +23 -0
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +23 -0
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +23 -0
  280. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +23 -0
  281. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +23 -0
  282. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +23 -0
  283. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +23 -0
  284. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +23 -0
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +23 -0
  286. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +23 -0
  287. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +23 -0
  288. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +23 -0
  289. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +23 -0
  290. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +23 -0
  291. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +23 -0
  292. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +23 -0
  293. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +23 -0
  294. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +23 -0
  295. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +23 -0
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +23 -0
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +23 -0
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +23 -0
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +23 -0
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +23 -0
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +23 -0
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +23 -0
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +23 -0
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +23 -0
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +23 -0
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +23 -0
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +23 -0
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +23 -0
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +23 -0
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +23 -0
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +23 -0
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +23 -0
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +23 -0
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +23 -0
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +23 -0
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +23 -0
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +23 -0
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +23 -0
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +23 -0
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +23 -0
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +23 -0
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +23 -0
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +23 -0
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +305 -5
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +24 -1
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +23 -0
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +136 -3
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +41 -1
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +40 -0
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +23 -0
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +23 -0
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +23 -0
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +23 -0
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +23 -0
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +23 -0
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +23 -0
  337. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +23 -0
  338. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +23 -0
  339. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +23 -0
  340. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +23 -0
  341. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +23 -0
  342. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +23 -0
  343. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +23 -0
  344. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +23 -0
  345. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +23 -0
  346. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +23 -0
  347. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +23 -0
  348. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +23 -0
  349. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +23 -0
  350. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +23 -0
  351. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +23 -0
  352. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +23 -0
  353. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +23 -0
  354. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +23 -0
  355. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +23 -0
  356. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +23 -0
  357. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +23 -0
  358. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +23 -0
  359. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +23 -0
  360. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +23 -0
  361. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +23 -0
  362. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +23 -0
  363. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +23 -0
  364. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +23 -0
  365. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +23 -0
  366. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +23 -0
  367. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +23 -0
  368. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +23 -0
  369. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +23 -0
  370. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +23 -0
  371. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +23 -0
  372. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +23 -0
  373. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +23 -0
  374. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +23 -0
  375. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +23 -0
  376. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +23 -0
  377. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +23 -0
  378. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +23 -0
  379. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +23 -0
  380. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +23 -0
  381. nautobot/project-static/docs/user-guide/index.html +23 -0
  382. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +23 -0
  383. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +23 -0
  384. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +23 -0
  385. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +23 -0
  386. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +23 -0
  387. nautobot/project-static/docs/user-guide/platform-functionality/events.html +23 -0
  388. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +23 -0
  389. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +23 -0
  390. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +23 -0
  391. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +23 -0
  392. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +23 -0
  393. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +23 -0
  394. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +23 -0
  395. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +24 -1
  396. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +23 -0
  397. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +23 -0
  398. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +23 -0
  399. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +23 -0
  400. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +23 -0
  401. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +23 -0
  402. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +23 -0
  403. nautobot/project-static/docs/user-guide/platform-functionality/note.html +23 -0
  404. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +23 -0
  405. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +23 -0
  406. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +23 -0
  407. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +23 -0
  408. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +23 -0
  409. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +23 -0
  410. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +23 -0
  411. nautobot/project-static/docs/user-guide/platform-functionality/role.html +23 -0
  412. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +23 -0
  413. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +23 -0
  414. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +23 -0
  415. nautobot/project-static/docs/user-guide/platform-functionality/status.html +23 -0
  416. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +23 -0
  417. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +23 -0
  418. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +23 -0
  419. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +23 -0
  420. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +23 -0
  421. nautobot/users/tests/test_api.py +2 -2
  422. nautobot/virtualization/templates/virtualization/virtualmachine.html +2 -252
  423. nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +2 -75
  424. nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +252 -0
  425. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +75 -0
  426. nautobot/virtualization/urls.py +3 -61
  427. nautobot/virtualization/views.py +48 -72
  428. {nautobot-2.4.14.dist-info → nautobot-2.4.15.dist-info}/METADATA +24 -24
  429. {nautobot-2.4.14.dist-info → nautobot-2.4.15.dist-info}/RECORD +433 -416
  430. {nautobot-2.4.14.dist-info → nautobot-2.4.15.dist-info}/LICENSE.txt +0 -0
  431. {nautobot-2.4.14.dist-info → nautobot-2.4.15.dist-info}/NOTICE +0 -0
  432. {nautobot-2.4.14.dist-info → nautobot-2.4.15.dist-info}/WHEEL +0 -0
  433. {nautobot-2.4.14.dist-info → nautobot-2.4.15.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
@@ -566,132 +566,45 @@ class ObjectAssignContactOrTeamView(generic.ObjectEditView):
566
566
  #
567
567
 
568
568
 
569
- class CustomFieldListView(generic.ObjectListView):
569
+ class CustomFieldUIViewSet(NautobotUIViewSet):
570
+ bulk_update_form_class = forms.CustomFieldBulkEditForm
570
571
  queryset = CustomField.objects.all()
571
- table = tables.CustomFieldTable
572
- filterset = filters.CustomFieldFilterSet
573
- filterset_form = forms.CustomFieldFilterForm
572
+ serializer_class = serializers.CustomFieldSerializer
573
+ filterset_class = filters.CustomFieldFilterSet
574
+ filterset_form_class = forms.CustomFieldFilterForm
575
+ form_class = forms.CustomFieldForm
576
+ table_class = tables.CustomFieldTable
577
+ template_name = "extras/customfield_update.html"
574
578
  action_buttons = ("add",)
575
579
 
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
580
  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)
581
+ context = super().get_extra_context(request, instance)
635
582
 
636
- return redirect(request.get_full_path())
583
+ if self.action in ("create", "update"):
584
+ if request.POST:
585
+ context["choices"] = forms.CustomFieldChoiceFormSet(data=request.POST, instance=instance)
586
+ else:
587
+ context["choices"] = forms.CustomFieldChoiceFormSet(instance=instance)
637
588
 
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))
589
+ return context
643
590
 
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()
591
+ def form_save(self, form, **kwargs):
592
+ obj = super().form_save(form, **kwargs)
661
593
 
594
+ # Process the formset for choices
595
+ ctx = self.get_extra_context(self.request, obj)
596
+ choices = ctx["choices"]
597
+ if choices.is_valid():
598
+ choices.save()
662
599
  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
-
600
+ raise ValidationError(choices.errors)
682
601
 
683
- class CustomFieldBulkDeleteView(generic.BulkDeleteView):
684
- queryset = CustomField.objects.all()
685
- table = tables.CustomFieldTable
686
- filterset = filters.CustomFieldFilterSet
687
- form = forms.CustomFieldBulkDeleteForm
602
+ return obj
688
603
 
689
604
 
690
605
  #
691
606
  # Custom Links
692
607
  #
693
-
694
-
695
608
  class CustomLinkUIViewSet(NautobotUIViewSet):
696
609
  bulk_update_form_class = forms.CustomLinkBulkEditForm
697
610
  filterset_class = filters.CustomLinkFilterSet
@@ -2140,59 +2053,57 @@ class JobHookUIViewSet(NautobotUIViewSet):
2140
2053
  #
2141
2054
 
2142
2055
 
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):
2056
+ class JobResultUIViewSet(
2057
+ ObjectDetailViewMixin,
2058
+ ObjectListViewMixin,
2059
+ ObjectDestroyViewMixin,
2060
+ ObjectBulkDestroyViewMixin,
2061
+ ):
2062
+ filterset_class = filters.JobResultFilterSet
2063
+ filterset_form_class = forms.JobResultFilterForm
2064
+ serializer_class = serializers.JobResultSerializer
2065
+ table_class = tables.JobResultTable
2156
2066
  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"
2067
+ action_buttons = ()
2172
2068
 
2173
2069
  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
2070
+ context = super().get_extra_context(request, instance)
2071
+ if self.action == "retrieve":
2072
+ job_class = None
2073
+ if instance and instance.job_model:
2074
+ job_class = instance.job_model.job_class
2075
+
2076
+ context.update(
2077
+ {
2078
+ "job": job_class,
2079
+ "associated_record": None,
2080
+ "result": instance,
2081
+ }
2082
+ )
2178
2083
 
2179
- return {
2180
- "job": job_class,
2181
- "associated_record": associated_record,
2182
- "result": instance,
2183
- **super().get_extra_context(request, instance),
2184
- }
2084
+ return context
2185
2085
 
2086
+ def get_queryset(self):
2087
+ queryset = super().get_queryset().select_related("job_model", "user")
2186
2088
 
2187
- class JobLogEntryTableView(generic.GenericView):
2188
- """
2189
- Display a table of `JobLogEntry` objects for a given `JobResult` instance.
2190
- """
2089
+ if not self.detail:
2090
+ queryset = queryset.defer("result", "task_args", "task_kwargs", "celery_kwargs", "traceback", "meta")
2191
2091
 
2192
- queryset = JobResult.objects.all()
2092
+ return queryset
2093
+
2094
+ @action(
2095
+ detail=True,
2096
+ url_path="log-table",
2097
+ url_name="log-table",
2098
+ custom_view_base_action="view",
2099
+ )
2100
+ def log_table(self, request, pk=None):
2101
+ """
2102
+ Custom action to return a rendered JobLogEntry table for a JobResult.
2103
+ """
2193
2104
 
2194
- def get(self, request, pk=None):
2195
2105
  instance = get_object_or_404(self.queryset.restrict(request.user, "view"), pk=pk)
2106
+
2196
2107
  filter_q = request.GET.get("q")
2197
2108
  if filter_q:
2198
2109
  queryset = instance.job_log_entries.filter(
@@ -2200,14 +2111,15 @@ class JobLogEntryTableView(generic.GenericView):
2200
2111
  )
2201
2112
  else:
2202
2113
  queryset = instance.job_log_entries.all()
2114
+
2203
2115
  log_table = tables.JobLogEntryTable(data=queryset, user=request.user)
2204
2116
  paginate = {
2205
2117
  "paginator_class": EnhancedPaginator,
2206
2118
  "per_page": get_paginate_count(request),
2207
2119
  }
2208
2120
  RequestConfig(request, paginate).configure(log_table)
2209
- table = log_table.as_html(request)
2210
- return HttpResponse(table)
2121
+
2122
+ return HttpResponse(log_table.as_html(request))
2211
2123
 
2212
2124
 
2213
2125
  #
@@ -2423,35 +2335,22 @@ class ObjectMetadataUIViewSet(
2423
2335
  #
2424
2336
 
2425
2337
 
2426
- class NoteView(generic.ObjectView):
2427
- queryset = Note.objects.all()
2428
-
2429
-
2430
- class NoteListView(generic.ObjectListView):
2431
- """
2432
- List Notes
2433
- """
2434
-
2338
+ class NoteUIViewSet(
2339
+ ObjectChangeLogViewMixin, ObjectDestroyViewMixin, ObjectDetailViewMixin, ObjectEditViewMixin, ObjectListViewMixin
2340
+ ):
2341
+ filterset_class = filters.NoteFilterSet
2342
+ filterset_form_class = forms.NoteFilterForm
2343
+ form_class = forms.NoteForm
2435
2344
  queryset = Note.objects.all()
2436
- filterset = filters.NoteFilterSet
2437
- filterset_form = forms.NoteFilterForm
2438
- table = tables.NoteTable
2345
+ serializer_class = serializers.NoteSerializer
2346
+ table_class = tables.NoteTable
2439
2347
  action_buttons = ()
2440
2348
 
2441
-
2442
- class NoteEditView(generic.ObjectEditView):
2443
- queryset = Note.objects.all()
2444
- model_form = forms.NoteForm
2445
-
2446
2349
  def alter_obj(self, obj, request, url_args, url_kwargs):
2447
2350
  obj.user = request.user
2448
2351
  return obj
2449
2352
 
2450
2353
 
2451
- class NoteDeleteView(generic.ObjectDeleteView):
2452
- queryset = Note.objects.all()
2453
-
2454
-
2455
2354
  class ObjectNotesView(generic.GenericView):
2456
2355
  """
2457
2356
  Present a list of notes associated to a particular object.