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
@@ -15,6 +15,7 @@ from nautobot.core.filters import (
15
15
  ContentTypeChoiceFilter,
16
16
  ContentTypeFilter,
17
17
  ContentTypeMultipleChoiceFilter,
18
+ NaturalKeyOrPKMultipleChoiceFilter,
18
19
  RelatedMembershipBooleanFilter,
19
20
  SearchFilter,
20
21
  )
@@ -207,15 +208,29 @@ class FilterTestCases:
207
208
  qs_result = self.queryset.filter(**{f"{field_name}__in": test_data}).distinct()
208
209
  self.assertQuerysetEqualAndNotEmpty(filterset_result, qs_result, ordered=False)
209
210
 
211
+ def test_automagic_filters(self):
212
+ """https://github.com/nautobot/nautobot/issues/6656"""
213
+ self.assertIsNotNone(self.filterset)
214
+ fs = self.filterset() # pylint: disable=not-callable
215
+ if getattr(self.queryset.model, "is_contact_associable_model", False):
216
+ self.assertIsInstance(fs.filters["contacts"], NaturalKeyOrPKMultipleChoiceFilter)
217
+ self.assertIsInstance(fs.filters["contacts__n"], NaturalKeyOrPKMultipleChoiceFilter)
218
+ self.assertIsInstance(fs.filters["teams"], NaturalKeyOrPKMultipleChoiceFilter)
219
+ self.assertIsInstance(fs.filters["teams__n"], NaturalKeyOrPKMultipleChoiceFilter)
220
+
221
+ if getattr(self.queryset.model, "is_dynamic_group_associable_model", False):
222
+ self.assertIsInstance(fs.filters["dynamic_groups"], NaturalKeyOrPKMultipleChoiceFilter)
223
+ self.assertIsInstance(fs.filters["dynamic_groups__n"], NaturalKeyOrPKMultipleChoiceFilter)
224
+
210
225
  def test_boolean_filters_generic(self):
211
- """Test all `RelatedMembershipBooleanFilter` filters found in `self.filterset.get_filters()`
226
+ """Test all `RelatedMembershipBooleanFilter` filters found in `self.filterset.filters`
212
227
  except for the ones with custom filter logic defined in its `method` attribute.
213
228
 
214
229
  This test asserts that `filter=True` matches `self.queryset.filter(field__isnull=False)` and
215
230
  that `filter=False` matches `self.queryset.filter(field__isnull=True)`.
216
231
  """
217
232
  self.assertIsNotNone(self.filterset)
218
- for filter_name, filter_object in self.filterset.get_filters().items():
233
+ for filter_name, filter_object in self.filterset().filters.items(): # pylint: disable=not-callable
219
234
  if not isinstance(filter_object, RelatedMembershipBooleanFilter):
220
235
  continue
221
236
  if filter_object.method is not None:
@@ -380,6 +395,8 @@ class FilterTestCases:
380
395
  self._assert_q_filter_predicate_validity(obj, obj_field_name, filter_field_name, lookup_method)
381
396
 
382
397
  def test_content_type_related_fields_uses_content_type_filter(self):
398
+ self.assertIsNotNone(self.filterset)
399
+ fs = self.filterset() # pylint: disable=not-callable
383
400
  for field in self.queryset.model._meta.fields:
384
401
  related_model = getattr(field, "related_model", None)
385
402
  if not related_model or related_model != ContentType:
@@ -387,7 +404,7 @@ class FilterTestCases:
387
404
  with self.subTest(
388
405
  f"Assert {self.filterset.__class__.__name__}.{field.name} implements ContentTypeFilter"
389
406
  ):
390
- filter_field = self.filterset.get_filters().get(field.name)
407
+ filter_field = fs.filters.get(field.name)
391
408
  if not filter_field:
392
409
  # This field is not part of the Filterset.
393
410
  continue
@@ -24,7 +24,7 @@ class FormTestCases:
24
24
  self.skipTest(f"{self.form_class.__name__}.{field_name} has no query_params")
25
25
  field_model = field_class.queryset.model
26
26
  filterset_class = get_filterset_for_model(field_model)
27
- filterset_fields = set(filterset_class.get_filters().keys())
27
+ filterset_fields = set(filterset_class().filters.keys())
28
28
  invalid_query_params = query_params_fields - filterset_fields
29
29
  self.assertFalse(
30
30
  invalid_query_params,
@@ -148,7 +148,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
148
148
 
149
149
  def test_input_field_gets_updated(self):
150
150
  """Assert that a filter input/select field on Dynamic Filter Form updates if same field is updated."""
151
- self.browser.visit(f'{self.live_server_url}{reverse("dcim:location_list")}')
151
+ self.browser.visit(f"{self.live_server_url}{reverse('dcim:location_list')}")
152
152
 
153
153
  text_field_name = self.custom_fields[0].add_prefix_to_cf_key()
154
154
  integer_field_name = self.custom_fields[1].add_prefix_to_cf_key()
@@ -225,7 +225,7 @@ class ListViewFilterTestCase(SeleniumTestCase):
225
225
  def test_advanced_filter_application(self):
226
226
  """Assert that filters are applied successfully when using the advanced filter."""
227
227
  # Go to the location list view
228
- self.browser.visit(f'{self.live_server_url}{reverse("dcim:location_list")}')
228
+ self.browser.visit(f"{self.live_server_url}{reverse('dcim:location_list')}")
229
229
  # create a new tag
230
230
  tag = Tag.objects.create(name="Tag1")
231
231
  tag.content_types.set([ContentType.objects.get_for_model(Location)])
@@ -0,0 +1,366 @@
1
+ """
2
+ Unit tests for the updated breadcrumbs.py following Nautobot testing conventions.
3
+ """
4
+
5
+ from operator import itemgetter
6
+ from unittest.mock import patch
7
+
8
+ from django.template import Context
9
+ from django.utils.http import urlencode
10
+
11
+ from nautobot.core.testing import TestCase
12
+ from nautobot.core.ui.breadcrumbs import (
13
+ BaseBreadcrumbItem,
14
+ Breadcrumbs,
15
+ InstanceBreadcrumbItem,
16
+ ModelBreadcrumbItem,
17
+ ViewNameBreadcrumbItem,
18
+ )
19
+ from nautobot.dcim.models import Device, LocationType
20
+
21
+
22
+ class BreadcrumbItemsTestCase(TestCase):
23
+ """Test cases for BreadcrumbItem class."""
24
+
25
+ @classmethod
26
+ def setUpTestData(cls):
27
+ """Create test data."""
28
+ cls.location_type = LocationType.objects.create(name="Test Location Type Breadcrumbs")
29
+
30
+ def test_view_name_item(self):
31
+ """Test breadcrumb view name item."""
32
+ item = ViewNameBreadcrumbItem(view_name="home", label="Home")
33
+ context = Context({})
34
+
35
+ url, label = item.as_pair(context)
36
+
37
+ self.assertEqual(url, "/")
38
+ self.assertEqual(label, "Home")
39
+
40
+ def test_view_name_item_with_kwargs_and_query_params(self):
41
+ """Test breadcrumb view name item and kwargs."""
42
+ item = ViewNameBreadcrumbItem(
43
+ view_name="dcim:locationtype",
44
+ reverse_kwargs={"pk": self.location_type.pk},
45
+ reverse_query_params={"name": "test"},
46
+ label="Filtered Locations Types",
47
+ )
48
+ context = Context({})
49
+
50
+ url, label = item.as_pair(context)
51
+
52
+ self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/?name=test")
53
+ self.assertEqual(label, "Filtered Locations Types")
54
+
55
+ def test_view_name_item_with_kwargs_and_query_params_callable(self):
56
+ """Test breadcrumb view name item and kwargs."""
57
+ item = ViewNameBreadcrumbItem(
58
+ view_name="dcim:locationtype",
59
+ reverse_kwargs=lambda c: {"pk": c["object"].pk},
60
+ reverse_query_params=lambda c: {"name": c["object"].name},
61
+ label="Filtered Locations Types",
62
+ )
63
+ context = Context({"object": self.location_type})
64
+
65
+ url, label = item.as_pair(context)
66
+
67
+ self.assertEqual(
68
+ url, f"/dcim/location-types/{self.location_type.pk}/?{urlencode({'name': self.location_type.name})}"
69
+ )
70
+ self.assertEqual(label, "Filtered Locations Types")
71
+
72
+ def test_callable_label_and_view_name(self):
73
+ """Test label and view_name as callables."""
74
+ item = ViewNameBreadcrumbItem(
75
+ view_name=lambda _: "home",
76
+ label=lambda context: f"Hi, {context['user']}!",
77
+ )
78
+ context = Context({"user": "Frodo"})
79
+ url, label = item.as_pair(context)
80
+ self.assertEqual(url, "/")
81
+ self.assertEqual(label, "Hi, Frodo!")
82
+
83
+ def test_model_items(self):
84
+ """Test breadcrumb model items."""
85
+ test_cases = [
86
+ {
87
+ "name": "model_class",
88
+ "kwargs": {"model": Device},
89
+ "expected_url": "/dcim/devices/",
90
+ "expected_label": "Devices",
91
+ },
92
+ {
93
+ "name": "model_instance",
94
+ "kwargs": {"model": self.location_type},
95
+ "expected_url": "/dcim/location-types/",
96
+ "expected_label": "Location Types",
97
+ },
98
+ {
99
+ "name": "model_instance_callable",
100
+ "kwargs": {"model": itemgetter("object")},
101
+ "expected_url": "/dcim/location-types/",
102
+ "expected_label": "Location Types",
103
+ },
104
+ {
105
+ "name": "model_str",
106
+ "kwargs": {"model": "dcim.device"},
107
+ "expected_url": "/dcim/devices/",
108
+ "expected_label": "Devices",
109
+ },
110
+ {
111
+ "name": "model_class_custom_url_action",
112
+ "kwargs": {"model": Device, "action": "add", "label_type": "singular"},
113
+ "expected_url": "/dcim/devices/add/",
114
+ "expected_label": "Device",
115
+ },
116
+ {
117
+ "name": "model_class_with_kwargs",
118
+ "kwargs": {
119
+ "model": Device,
120
+ "action": "",
121
+ "label_type": "singular",
122
+ "reverse_kwargs": {"pk": "947a8a80-9e62-5605-ab18-7a47c588f0ad"},
123
+ },
124
+ "expected_url": "/dcim/devices/947a8a80-9e62-5605-ab18-7a47c588f0ad/",
125
+ "expected_label": "Device",
126
+ },
127
+ {
128
+ "name": "model_class_with_query_params",
129
+ "kwargs": {"model": Device, "reverse_query_params": {"filter": "abc"}},
130
+ "expected_url": "/dcim/devices/?filter=abc",
131
+ "expected_label": "Devices",
132
+ },
133
+ {
134
+ "name": "model_class_with_query_params_callable",
135
+ "kwargs": {
136
+ "model": itemgetter("model_type"),
137
+ "reverse_query_params": lambda c: {"name": c["device_name"]},
138
+ },
139
+ "expected_url": "/dcim/devices/?name=abc",
140
+ "expected_label": "Devices",
141
+ },
142
+ ]
143
+ for test_case in test_cases:
144
+ with self.subTest(action=test_case["name"]):
145
+ item = ModelBreadcrumbItem(**test_case["kwargs"])
146
+ context = Context({"object": self.location_type, "model_type": Device, "device_name": "abc"})
147
+
148
+ url, label = item.as_pair(context)
149
+
150
+ self.assertEqual(url, test_case["expected_url"])
151
+ self.assertEqual(label, test_case["expected_label"])
152
+
153
+ def test_model_item_from_context(self):
154
+ """Test breadcrumb item with model from context."""
155
+ item = ModelBreadcrumbItem(model_key="object")
156
+ context = Context({"object": self.location_type})
157
+
158
+ url, label = item.as_pair(context)
159
+
160
+ self.assertEqual(url, "/dcim/location-types/")
161
+ self.assertEqual(label, "Location Types")
162
+
163
+ def test_instance_item(self):
164
+ """Test breadcrumb item with instance from context."""
165
+ item = InstanceBreadcrumbItem(instance_key="object")
166
+ context = Context({"object": self.location_type})
167
+
168
+ url, label = item.as_pair(context)
169
+
170
+ self.assertEqual(url, f"/dcim/location-types/{self.location_type.pk}/")
171
+ self.assertEqual(label, str(self.location_type))
172
+
173
+ def test_label_override(self):
174
+ """Test that explicit label overrides automatic label generation."""
175
+ items = [
176
+ ViewNameBreadcrumbItem(view_name="dcim:locationtype", label="Custom Label"),
177
+ ModelBreadcrumbItem(model=LocationType, label="Custom Label"),
178
+ InstanceBreadcrumbItem(label="Custom Label"),
179
+ ]
180
+
181
+ context = Context({"object": self.location_type})
182
+
183
+ for item in items:
184
+ with self.subTest():
185
+ _, label = item.as_pair(context)
186
+ self.assertEqual(label, "Custom Label")
187
+
188
+ def test_no_reverse_match(self):
189
+ """Test handling of NoReverseMatch exception."""
190
+ item = ViewNameBreadcrumbItem(view_name="nonexistent")
191
+ context = Context({})
192
+
193
+ url, label = item.as_pair(context)
194
+
195
+ self.assertEqual(url, "")
196
+ self.assertEqual(label, "")
197
+
198
+ def test_empty_context_keys(self):
199
+ """Test breadcrumb item when context keys are missing."""
200
+ context = Context({})
201
+ item = InstanceBreadcrumbItem(instance_key="missing_key")
202
+
203
+ url, label = item.as_pair(context)
204
+ self.assertEqual(url, "")
205
+ self.assertEqual(label, "")
206
+
207
+ item = ModelBreadcrumbItem(model_key="missing_key")
208
+ url, label = item.as_pair(context)
209
+ self.assertEqual(url, "")
210
+ self.assertEqual(label, "")
211
+
212
+
213
+ class BreadcrumbsTestCase(TestCase):
214
+ """Test cases for Breadcrumbs class."""
215
+
216
+ @classmethod
217
+ def setUpTestData(cls):
218
+ """Create test data."""
219
+ cls.location_type = LocationType.objects.create(name="Test Location Type Breadcrumbs")
220
+
221
+ def test_default_initialization(self):
222
+ """Test that Breadcrumbs initializes with default values."""
223
+ breadcrumbs = Breadcrumbs()
224
+
225
+ # Should have defaults for list and details
226
+ self.assertEqual(len(breadcrumbs.items["list"]), 2)
227
+ self.assertEqual(len(breadcrumbs.items["detail"]), 3)
228
+
229
+ # Verify adding items
230
+ new_item = BaseBreadcrumbItem()
231
+ breadcrumbs = Breadcrumbs(items={"detail": [new_item], "list": [new_item], "custom_action": [new_item]})
232
+
233
+ self.assertEqual(len(breadcrumbs.items["list"]), 1)
234
+ self.assertEqual(breadcrumbs.items["list"][0], new_item)
235
+
236
+ self.assertEqual(len(breadcrumbs.items["detail"]), 2)
237
+ self.assertEqual(breadcrumbs.items["detail"][0], new_item)
238
+
239
+ self.assertEqual(len(breadcrumbs.items["custom_action"]), 1)
240
+ self.assertEqual(breadcrumbs.items["custom_action"][0], new_item)
241
+
242
+ def test_custom_items(self):
243
+ """Test Breadcrumbs with custom items."""
244
+ custom_list_item = ViewNameBreadcrumbItem(view_name="home", label="Home")
245
+ custom_items = {
246
+ "list": [ViewNameBreadcrumbItem(view_name="home", label="Home")],
247
+ }
248
+ breadcrumbs = Breadcrumbs(items=custom_items)
249
+
250
+ self.assertEqual(len(breadcrumbs.items["list"]), 1)
251
+ self.assertEqual(breadcrumbs.items["list"][0], custom_list_item)
252
+
253
+ # Other defaults should still exist
254
+ self.assertIn("detail", breadcrumbs.items)
255
+ self.assertEqual(len(breadcrumbs.items["detail"]), 3)
256
+
257
+ def test_get_items_from_action_static_method(self):
258
+ """Test the _get_items_from_action static method."""
259
+ test_items = {
260
+ "list": [ViewNameBreadcrumbItem(view_name="", label="List Item")],
261
+ "detail": [ViewNameBreadcrumbItem(view_name="", label="Detail Item")],
262
+ }
263
+
264
+ # Test specific action found
265
+ result = Breadcrumbs.get_items_for_action(test_items, "list", False)
266
+ self.assertEqual(len(result), 1)
267
+ self.assertEqual(result[0].label, "List Item")
268
+
269
+ # Test fallback to detail when action not found and detail=True
270
+ result = Breadcrumbs.get_items_for_action(test_items, "nonexistent", True)
271
+ self.assertEqual(len(result), 1)
272
+ self.assertEqual(result[0].label, "Detail Item")
273
+
274
+ # Test no fallback when detail=False
275
+ result = Breadcrumbs.get_items_for_action(test_items, "nonexistent", False)
276
+ self.assertEqual(len(result), 0)
277
+ self.assertEqual(result, [])
278
+
279
+ def test_detail_fallback_behavior(self):
280
+ """Test that detail fallback works correctly in get_breadcrumbs_items."""
281
+ breadcrumbs = Breadcrumbs()
282
+ expected_items = [
283
+ ("/dcim/location-types/", "Location Types"),
284
+ (f"/dcim/location-types/{self.location_type.pk}/", str(self.location_type)),
285
+ ]
286
+
287
+ # Test with an action that doesn't exist but detail=True
288
+ context = Context(
289
+ {"view_action": "custom_action", "detail": True, "model": LocationType, "object": self.location_type}
290
+ )
291
+
292
+ items = breadcrumbs.get_breadcrumbs_items(context)
293
+
294
+ # Should get 2 items from detail fallback
295
+ self.assertEqual(len(items), 2)
296
+ self.assertEqual(items, expected_items)
297
+
298
+ def test_render_method(self):
299
+ """Test the render method."""
300
+ breadcrumbs = Breadcrumbs()
301
+ context = Context({"view_action": "list", "model": Device})
302
+
303
+ html = breadcrumbs.render(context)
304
+
305
+ expected_html = """<ol class="breadcrumb"><li><a href="/dcim/devices/">Devices</a></li></ol>"""
306
+ self.assertHTMLEqual(html, expected_html)
307
+
308
+ def test_get_extra_context(self):
309
+ """Test that get_extra_context can be extended."""
310
+
311
+ class CustomBreadcrumbs(Breadcrumbs):
312
+ def get_extra_context(self, context: Context):
313
+ return {"custom_key": "custom_value"}
314
+
315
+ render_context = {}
316
+
317
+ def capture_context(template, context, **kwargs):
318
+ nonlocal render_context
319
+ render_context = context.flatten()
320
+ return ""
321
+
322
+ breadcrumbs = CustomBreadcrumbs(items={"list": []})
323
+ context = Context({})
324
+
325
+ with patch("nautobot.core.ui.breadcrumbs.render_component_template", side_effect=capture_context):
326
+ breadcrumbs.render(context)
327
+
328
+ # Check that custom context was passed
329
+ self.assertEqual(render_context.get("custom_key"), "custom_value")
330
+
331
+ def test_should_render_skips_items(self):
332
+ """Breadcrumbs should skip items where should_render(context) is False."""
333
+
334
+ item_visible = ViewNameBreadcrumbItem(view_name="home", label="Visible", should_render=lambda _: True)
335
+ item_hidden = ViewNameBreadcrumbItem(view_name="home", label="Hidden", should_render=lambda _: False)
336
+ breadcrumbs = Breadcrumbs(items={"custom_action": [item_visible, item_hidden]})
337
+ context = Context({"view_action": "custom_action"})
338
+
339
+ items = breadcrumbs.get_breadcrumbs_items(context)
340
+ self.assertEqual(len(items), 1)
341
+ self.assertIn(("/", "Visible"), items)
342
+ self.assertNotIn(("/", "Hidden"), items)
343
+
344
+ def test_filter_breadcrumbs_items_removes_empty_pairs(self):
345
+ """filter_breadcrumbs_items should remove items where label is empty or None."""
346
+
347
+ breadcrumbs = Breadcrumbs()
348
+ # (url, label) pairs: only the last should remain
349
+ pairs = [
350
+ ("", ""), # empty
351
+ ("", " "), # whitespace
352
+ ("", "\t"), # whitespace
353
+ ("", "Non-empty"), # label not empty
354
+ ("/foo", ""), # url not empty
355
+ ("/foo", "Label"), # url, label not empty
356
+ (None, None), # both None
357
+ (None, "Label"), # label not None
358
+ ("/bar", None), # url not None
359
+ ]
360
+ expected = [
361
+ ("", "Non-empty"),
362
+ ("/foo", "Label"),
363
+ (None, "Label"),
364
+ ]
365
+ filtered = breadcrumbs.filter_breadcrumbs_items(pairs, Context({}))
366
+ self.assertEqual(filtered, expected)
@@ -1,9 +1,13 @@
1
1
  from io import StringIO
2
2
 
3
3
  from django.core.management import call_command
4
+ from django.utils.timezone import now
4
5
  import yaml
5
6
 
7
+ from nautobot.core.management.commands.check_job_approval_status import ApprovalRequiredScheduledJobsError
6
8
  from nautobot.core.testing import TestCase
9
+ from nautobot.extras.choices import JobExecutionType
10
+ from nautobot.extras.models import Job, ScheduledJob
7
11
 
8
12
 
9
13
  class ManagementCommandTestCase(TestCase):
@@ -29,3 +33,39 @@ class ManagementCommandTestCase(TestCase):
29
33
  self.assertHttpStatus(
30
34
  response, 200, f"{view_name}: {endpoint} returns status Code {response.status_code} instead of 200"
31
35
  )
36
+
37
+ def test_check_job_approval_status_no_jobs(self):
38
+ out = StringIO()
39
+ # update all jobs to not have approval_required=True
40
+ Job.objects.update(approval_required=False)
41
+ call_command("check_job_approval_status", stdout=out)
42
+ output = out.getvalue()
43
+ self.assertIn("No approval_required jobs or scheduled jobs found.", output)
44
+
45
+ def test_check_job_approval_status_with__with_approval_required_jobs(self):
46
+ out = StringIO()
47
+ self.assertTrue(Job.objects.filter(approval_required=True).exists())
48
+ self.assertFalse(ScheduledJob.objects.filter(approval_required=True).exists())
49
+ call_command("check_job_approval_status", stdout=out)
50
+ output = out.getvalue()
51
+ self.assertIn("Following jobs still have `approval_required=True`.", output)
52
+
53
+ def test_check_job_approval_status_with_approval_required_scheduled_jobs(self):
54
+ job = Job.objects.first()
55
+ scheduled_job = ScheduledJob.objects.create(
56
+ name="Scheduled Job",
57
+ task="test_managment_command.TestManagmentCommand",
58
+ job_model=job,
59
+ interval=JobExecutionType.TYPE_IMMEDIATELY,
60
+ user=self.user,
61
+ approval_required=True,
62
+ start_time=now(),
63
+ )
64
+ self.assertTrue(ScheduledJob.objects.filter(approval_required=True).exists())
65
+ with self.assertRaises(ApprovalRequiredScheduledJobsError) as cm:
66
+ call_command("check_job_approval_status")
67
+
68
+ self.assertIn(
69
+ "These need to be approved (and run) or denied before upgrading to Nautobot v3", str(cm.exception)
70
+ )
71
+ self.assertIn(scheduled_job.name, str(cm.exception))
@@ -19,10 +19,60 @@ from nautobot.core.utils import lookup
19
19
  from nautobot.dcim import choices as dcim_choices, filters as dcim_filters, models as dcim_models
20
20
  from nautobot.dcim.models import Controller, Device
21
21
  from nautobot.extras import models as extras_models
22
- from nautobot.extras.utils import FeatureQuery
22
+ from nautobot.extras.utils import FeatureQuery, RoleModelsQuery
23
23
  from nautobot.ipam import models as ipam_models
24
24
 
25
25
 
26
+ class ContentTypeMultipleChoiceFilterTest(testing.TestCase):
27
+ class RoleFilterSet(filters.BaseFilterSet):
28
+ content_types = filters.ContentTypeMultipleChoiceFilter(
29
+ choices=RoleModelsQuery().get_choices,
30
+ conjoined=False,
31
+ )
32
+
33
+ class Meta:
34
+ model = extras_models.Role
35
+ fields = ["content_types"]
36
+
37
+ def test_filter_variations(self):
38
+ with self.subTest("single label"):
39
+ filterset = self.RoleFilterSet({"content_types": ["ipam.ipaddress"]}, extras_models.Role.objects.all())
40
+ qs = extras_models.Role.objects.filter(
41
+ content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)]
42
+ )
43
+ self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
44
+
45
+ with self.subTest("multiple labels"):
46
+ filterset = self.RoleFilterSet(
47
+ {"content_types": ["ipam.ipaddress", "dcim.rack"]}, extras_models.Role.objects.all()
48
+ )
49
+ # remember, conjoined=False
50
+ qs = extras_models.Role.objects.filter(
51
+ django_models.Q(content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)])
52
+ | django_models.Q(content_types__in=[ContentType.objects.get_for_model(dcim_models.Rack)])
53
+ ).distinct()
54
+ self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
55
+
56
+ with self.subTest("exclude single label"):
57
+ filterset = self.RoleFilterSet({"content_types__n": ["ipam.ipaddress"]}, extras_models.Role.objects.all())
58
+ qs = extras_models.Role.objects.exclude(
59
+ content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)]
60
+ )
61
+ self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
62
+
63
+ with self.subTest("exclude multiple labels"):
64
+ filterset = self.RoleFilterSet(
65
+ {"content_types__n": ["ipam.ipaddress", "dcim.rack"]}, extras_models.Role.objects.all()
66
+ )
67
+ self.assertTrue(filterset.is_valid(), filterset.errors)
68
+ # remember, conjoined=False
69
+ qs = extras_models.Role.objects.exclude(
70
+ django_models.Q(content_types__in=[ContentType.objects.get_for_model(ipam_models.IPAddress)])
71
+ | django_models.Q(content_types__in=[ContentType.objects.get_for_model(dcim_models.Rack)])
72
+ ).distinct()
73
+ self.assertQuerysetEqualAndNotEmpty(filterset.qs, qs)
74
+
75
+
26
76
  class TreeNodeMultipleChoiceFilterTest(TestCase):
27
77
  class LocationFilterSet(filters.BaseFilterSet):
28
78
  parent = filters.TreeNodeMultipleChoiceFilter(queryset=dcim_models.Location.objects.all())
@@ -581,7 +581,7 @@ class JSONFieldTest(testing.TestCase):
581
581
  device_content_type = ContentType.objects.get_for_model(dcim_models.Device)
582
582
  custom_field.content_types.set([device_content_type])
583
583
  # Fetch URL with filter parameter
584
- response = self.client.get(f'{reverse("dcim:device_list")}?name=Foo%20Device')
584
+ response = self.client.get(f"{reverse('dcim:device_list')}?name=Foo%20Device")
585
585
  self.assertIn("Foo Device", str(response.content))
586
586
 
587
587
  def test_prepare_value_with_utf8(self):
@@ -527,18 +527,18 @@ class GraphQLAPIPermissionTest(GraphQLTestCaseBase):
527
527
  for i in range(2):
528
528
  # Rack permission
529
529
  rack_obj_permission = ObjectPermission.objects.create(
530
- name=f"Permission Rack {i+1}",
530
+ name=f"Permission Rack {i + 1}",
531
531
  actions=["view", "add", "change", "delete"],
532
- constraints={"location__name": f"Location {i+1}"},
532
+ constraints={"location__name": f"Location {i + 1}"},
533
533
  )
534
534
  rack_obj_permission.object_types.add(rack_object_type)
535
535
  rack_obj_permission.groups.add(cls.groups[i])
536
536
  rack_obj_permission.users.add(cls.users[i])
537
537
 
538
538
  location_obj_permission = ObjectPermission.objects.create(
539
- name=f"Permission Location {i+1}",
539
+ name=f"Permission Location {i + 1}",
540
540
  actions=["view", "add", "change", "delete"],
541
- constraints={"name": f"Location {i+1}"},
541
+ constraints={"name": f"Location {i + 1}"},
542
542
  )
543
543
  location_obj_permission.object_types.add(location_object_type)
544
544
  location_obj_permission.groups.add(cls.groups[i])