nautobot 2.4.16__py3-none-any.whl → 2.4.18__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 (434) hide show
  1. nautobot/apps/utils.py +2 -0
  2. nautobot/apps/views.py +2 -0
  3. nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -8
  4. nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +9 -0
  5. nautobot/circuits/tests/integration/test_circuit.py +2 -2
  6. nautobot/circuits/views.py +32 -15
  7. nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +3 -3
  8. nautobot/cloud/views.py +7 -0
  9. nautobot/core/apps/__init__.py +1 -0
  10. nautobot/core/celery/__init__.py +2 -1
  11. nautobot/core/filters.py +2 -2
  12. nautobot/core/settings.py +1 -0
  13. nautobot/core/settings.yaml +9 -0
  14. nautobot/core/tables.py +21 -23
  15. nautobot/core/templates/components/breadcrumbs.html +19 -0
  16. nautobot/core/templates/components/panel/panel.html +1 -1
  17. nautobot/core/templates/generic/object_changelog.html +0 -2
  18. nautobot/core/templates/generic/object_list.html +15 -12
  19. nautobot/core/templates/generic/object_notes.html +0 -2
  20. nautobot/core/templates/generic/object_retrieve.html +16 -9
  21. nautobot/core/templates/inc/paginator.html +3 -3
  22. nautobot/core/templates/inc/table.html +2 -2
  23. nautobot/core/templatetags/helpers.py +104 -6
  24. nautobot/core/templatetags/ui_framework.py +40 -5
  25. nautobot/core/testing/filters.py +37 -21
  26. nautobot/core/testing/mixins.py +1 -1
  27. nautobot/core/testing/views.py +27 -4
  28. nautobot/core/tests/test_tables.py +43 -6
  29. nautobot/core/tests/test_templatetags_ui_framework.py +146 -0
  30. nautobot/core/tests/test_titles.py +2 -2
  31. nautobot/core/tests/test_ui.py +14 -1
  32. nautobot/core/tests/test_views.py +45 -0
  33. nautobot/core/ui/breadcrumbs.py +13 -8
  34. nautobot/core/ui/bulk_buttons.py +53 -53
  35. nautobot/core/ui/object_detail.py +52 -9
  36. nautobot/core/ui/titles.py +9 -5
  37. nautobot/core/utils/data.py +13 -0
  38. nautobot/core/utils/deprecation.py +2 -0
  39. nautobot/core/views/__init__.py +24 -3
  40. nautobot/core/views/generic.py +42 -17
  41. nautobot/core/views/mixins.py +146 -12
  42. nautobot/core/views/utils.py +117 -0
  43. nautobot/dcim/migrations/0073_alter_powerport_power_factor_and_more.py +41 -0
  44. nautobot/dcim/models/device_component_templates.py +4 -2
  45. nautobot/dcim/models/device_components.py +3 -2
  46. nautobot/dcim/models/devices.py +4 -0
  47. nautobot/dcim/tables/__init__.py +2 -0
  48. nautobot/dcim/tables/devices.py +24 -0
  49. nautobot/dcim/tables/power.py +2 -2
  50. nautobot/dcim/templates/dcim/device/base.html +1 -11
  51. nautobot/dcim/templates/dcim/device_component.html +0 -19
  52. nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -16
  53. nautobot/dcim/templates/dcim/rack_elevation_list.html +4 -4
  54. nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -50
  55. nautobot/dcim/tests/test_views.py +41 -0
  56. nautobot/dcim/views.py +169 -39
  57. nautobot/extras/filters/mixins.py +1 -1
  58. nautobot/extras/forms/forms.py +15 -0
  59. nautobot/extras/models/customfields.py +45 -9
  60. nautobot/extras/models/groups.py +10 -1
  61. nautobot/extras/models/jobs.py +2 -2
  62. nautobot/extras/plugins/views.py +18 -5
  63. nautobot/extras/tables.py +4 -2
  64. nautobot/extras/templates/extras/configcontext_retrieve.html +1 -1
  65. nautobot/extras/templates/extras/configcontext_update.html +49 -49
  66. nautobot/extras/templates/extras/configcontextschema_retrieve.html +47 -47
  67. nautobot/extras/templates/extras/configcontextschema_update.html +18 -18
  68. nautobot/extras/templates/extras/customfield_retrieve.html +1 -128
  69. nautobot/extras/templates/extras/dynamicgroup.html +2 -99
  70. nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -199
  71. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +99 -0
  72. nautobot/extras/templates/extras/dynamicgroup_update.html +199 -0
  73. nautobot/extras/templates/extras/gitrepository.html +2 -82
  74. nautobot/extras/templates/extras/gitrepository_object_edit.html +2 -13
  75. nautobot/extras/templates/extras/gitrepository_retrieve.html +82 -0
  76. nautobot/extras/templates/extras/gitrepository_update.html +13 -0
  77. nautobot/extras/templates/extras/inc/job_table.html +1 -1
  78. nautobot/extras/templates/extras/inc/object_contact_header.html +2 -2
  79. nautobot/extras/templates/extras/note_retrieve.html +1 -53
  80. nautobot/extras/templates/extras/plugin_detail.html +3 -7
  81. nautobot/extras/templates/extras/plugins_list.html +0 -2
  82. nautobot/extras/templates/extras/tag_retrieve.html +1 -1
  83. nautobot/extras/templates/extras/tag_update.html +14 -14
  84. nautobot/extras/templates/extras/team_retrieve.html +1 -1
  85. nautobot/extras/tests/test_dynamicgroups.py +73 -18
  86. nautobot/extras/tests/test_models.py +216 -0
  87. nautobot/extras/tests/test_views.py +7 -2
  88. nautobot/extras/urls.py +2 -94
  89. nautobot/extras/views.py +425 -430
  90. nautobot/ipam/apps.py +1 -0
  91. nautobot/ipam/jobs/__init__.py +10 -0
  92. nautobot/ipam/jobs/cleanup.py +296 -0
  93. nautobot/ipam/models.py +301 -178
  94. nautobot/ipam/querysets.py +3 -3
  95. nautobot/ipam/signals.py +6 -1
  96. nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +3 -3
  97. nautobot/ipam/templates/ipam/inc/toggle_available.html +2 -2
  98. nautobot/ipam/templates/ipam/ipaddress_assign.html +1 -1
  99. nautobot/ipam/templates/ipam/prefix.html +0 -8
  100. nautobot/ipam/templates/ipam/prefix_list.html +1 -1
  101. nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -77
  102. nautobot/ipam/tests/test_api.py +5 -0
  103. nautobot/ipam/tests/test_jobs.py +454 -0
  104. nautobot/ipam/tests/test_models.py +677 -122
  105. nautobot/ipam/tests/test_querysets.py +46 -0
  106. nautobot/ipam/tests/test_views.py +40 -164
  107. nautobot/ipam/urls.py +0 -11
  108. nautobot/ipam/utils/migrations.py +1 -1
  109. nautobot/ipam/utils/testing.py +9 -4
  110. nautobot/ipam/views.py +175 -235
  111. nautobot/project-static/docs/404.html +9 -6
  112. nautobot/project-static/docs/apps/index.html +9 -6
  113. nautobot/project-static/docs/apps/nautobot-apps.html +9 -6
  114. nautobot/project-static/docs/assets/javascripts/bundle.92b07e13.min.js +16 -0
  115. nautobot/project-static/docs/assets/javascripts/{bundle.50899def.min.js.map → bundle.92b07e13.min.js.map} +2 -2
  116. nautobot/project-static/docs/assets/javascripts/workers/{search.d50fe291.min.js → search.973d3a69.min.js} +4 -4
  117. nautobot/project-static/docs/assets/javascripts/workers/{search.d50fe291.min.js.map → search.973d3a69.min.js.map} +1 -1
  118. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +9 -6
  119. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +9 -6
  120. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +9 -6
  121. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +9 -6
  122. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +10 -7
  123. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +9 -6
  124. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +9 -6
  125. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +9 -6
  126. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +9 -6
  127. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +9 -6
  128. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +9 -6
  129. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +9 -6
  130. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +9 -6
  131. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +9 -6
  132. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +9 -6
  133. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -8
  134. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +9 -6
  135. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +9 -6
  136. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -8
  137. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +81 -6
  138. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +73 -18
  139. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +9 -6
  140. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +69 -7
  141. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +402 -21
  142. nautobot/project-static/docs/development/apps/api/configuration-view.html +13 -10
  143. nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -8
  144. nautobot/project-static/docs/development/apps/api/models/django-admin.html +13 -10
  145. nautobot/project-static/docs/development/apps/api/models/global-search.html +10 -7
  146. nautobot/project-static/docs/development/apps/api/models/graphql.html +18 -15
  147. nautobot/project-static/docs/development/apps/api/models/index.html +14 -11
  148. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +12 -9
  149. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +15 -12
  150. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +9 -6
  151. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +15 -12
  152. nautobot/project-static/docs/development/apps/api/platform-features/index.html +9 -6
  153. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -8
  154. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +16 -13
  155. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +12 -10305
  156. nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +10722 -0
  157. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +15 -12
  158. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +14 -11
  159. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +9 -6
  160. nautobot/project-static/docs/development/apps/api/prometheus.html +15 -12
  161. nautobot/project-static/docs/development/apps/api/setup.html +9 -6
  162. nautobot/project-static/docs/development/apps/api/testing.html +9 -6
  163. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +12 -9
  164. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +9 -6
  165. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +9 -6
  166. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +9 -6
  167. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +20 -17
  168. nautobot/project-static/docs/development/apps/api/views/base-template.html +9 -6
  169. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +15 -12
  170. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +14 -11
  171. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +9 -6
  172. nautobot/project-static/docs/development/apps/api/views/index.html +9 -6
  173. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +10 -7
  174. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +24 -21
  175. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +12 -9
  176. nautobot/project-static/docs/development/apps/api/views/notes.html +10 -7
  177. nautobot/project-static/docs/development/apps/api/views/rest-api.html +19 -16
  178. nautobot/project-static/docs/development/apps/api/views/urls.html +11 -8
  179. nautobot/project-static/docs/development/apps/index.html +9 -6
  180. nautobot/project-static/docs/development/apps/migration/code-updates.html +19 -16
  181. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +9 -6
  182. nautobot/project-static/docs/development/apps/migration/from-v1.html +9 -6
  183. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +22 -19
  184. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +9 -6
  185. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +9 -6
  186. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +9 -6
  187. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +9 -6
  188. nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +14 -11
  189. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +27 -24
  190. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +20 -17
  191. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +20 -17
  192. nautobot/project-static/docs/development/apps/porting-from-netbox.html +9 -6
  193. nautobot/project-static/docs/development/core/application-registry.html +23 -20
  194. nautobot/project-static/docs/development/core/best-practices.html +23 -20
  195. nautobot/project-static/docs/development/core/bootstrap-ui.html +9 -6
  196. nautobot/project-static/docs/development/core/caching.html +9 -6
  197. nautobot/project-static/docs/development/core/controllers.html +9 -6
  198. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +10 -7
  199. nautobot/project-static/docs/development/core/generic-views.html +9 -6
  200. nautobot/project-static/docs/development/core/getting-started.html +9 -21
  201. nautobot/project-static/docs/development/core/homepage.html +12 -9
  202. nautobot/project-static/docs/development/core/index.html +9 -6
  203. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +9 -6
  204. nautobot/project-static/docs/development/core/model-checklist.html +9 -6
  205. nautobot/project-static/docs/development/core/model-features.html +11 -8
  206. nautobot/project-static/docs/development/core/natural-keys.html +21 -18
  207. nautobot/project-static/docs/development/core/navigation-menu.html +10 -7
  208. nautobot/project-static/docs/development/core/release-checklist.html +9 -6
  209. nautobot/project-static/docs/development/core/role-internals.html +9 -6
  210. nautobot/project-static/docs/development/core/settings.html +9 -6
  211. nautobot/project-static/docs/development/core/style-guide.html +32 -29
  212. nautobot/project-static/docs/development/core/templates.html +9 -6
  213. nautobot/project-static/docs/development/core/testing.html +10 -7
  214. nautobot/project-static/docs/development/core/ui-component-framework.html +42 -44
  215. nautobot/project-static/docs/development/core/user-preferences.html +9 -6
  216. nautobot/project-static/docs/development/index.html +9 -6
  217. nautobot/project-static/docs/development/jobs/getting-started.html +13 -10
  218. nautobot/project-static/docs/development/jobs/index.html +9 -6
  219. nautobot/project-static/docs/development/jobs/installation.html +23 -20
  220. nautobot/project-static/docs/development/jobs/job-extensions.html +25 -22
  221. nautobot/project-static/docs/development/jobs/job-logging.html +12 -9
  222. nautobot/project-static/docs/development/jobs/job-patterns.html +45 -42
  223. nautobot/project-static/docs/development/jobs/job-structure.html +53 -50
  224. nautobot/project-static/docs/development/jobs/migration/from-v1.html +23 -20
  225. nautobot/project-static/docs/development/jobs/testing.html +14 -11
  226. nautobot/project-static/docs/index.html +9 -6
  227. nautobot/project-static/docs/objects.inv +0 -0
  228. nautobot/project-static/docs/overview/application_stack.html +9 -6
  229. nautobot/project-static/docs/overview/design_philosophy.html +9 -6
  230. nautobot/project-static/docs/release-notes/index.html +9 -6
  231. nautobot/project-static/docs/release-notes/version-1.0.html +9 -6
  232. nautobot/project-static/docs/release-notes/version-1.1.html +9 -6
  233. nautobot/project-static/docs/release-notes/version-1.2.html +10 -7
  234. nautobot/project-static/docs/release-notes/version-1.3.html +9 -6
  235. nautobot/project-static/docs/release-notes/version-1.4.html +9 -6
  236. nautobot/project-static/docs/release-notes/version-1.5.html +13 -10
  237. nautobot/project-static/docs/release-notes/version-1.6.html +9 -6
  238. nautobot/project-static/docs/release-notes/version-2.0.html +9 -6
  239. nautobot/project-static/docs/release-notes/version-2.1.html +9 -6
  240. nautobot/project-static/docs/release-notes/version-2.2.html +9 -6
  241. nautobot/project-static/docs/release-notes/version-2.3.html +9 -6
  242. nautobot/project-static/docs/release-notes/version-2.4.html +489 -6
  243. nautobot/project-static/docs/search/search_index.json +1 -1
  244. nautobot/project-static/docs/sitemap.xml +301 -301
  245. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  246. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +15 -12
  247. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +9 -6
  248. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +16 -13
  249. nautobot/project-static/docs/user-guide/administration/configuration/index.html +9 -6
  250. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +9 -6
  251. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +38 -8
  252. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +9 -6
  253. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +9 -6
  254. nautobot/project-static/docs/user-guide/administration/guides/docker.html +9 -6
  255. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +9 -6
  256. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +9 -6
  257. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +9 -6
  258. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +9 -6
  259. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +9 -6
  260. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +16 -13
  261. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +9 -6
  262. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +9 -6
  263. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +9 -6
  264. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +9 -6
  265. nautobot/project-static/docs/user-guide/administration/installation/index.html +9 -6
  266. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +9 -6
  267. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +9 -6
  268. nautobot/project-static/docs/user-guide/administration/installation/services.html +12 -9
  269. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +13 -10
  270. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +10 -7
  271. nautobot/project-static/docs/user-guide/administration/security/index.html +9 -6
  272. nautobot/project-static/docs/user-guide/administration/security/notices.html +9 -6
  273. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +9 -6
  274. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +10 -7
  275. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +9 -6
  276. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +9 -6
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +9 -6
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +9 -6
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +9 -6
  280. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +9 -6
  281. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +9 -6
  282. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +15 -12
  283. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +9 -6
  284. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +9 -6
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +9 -6
  286. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +9 -6
  287. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +9 -6
  288. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +9 -6
  289. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +9 -6
  290. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +9 -6
  291. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +9 -6
  292. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +9 -6
  293. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +9 -6
  294. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +9 -6
  295. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +9 -6
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +9 -6
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +9 -6
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +9 -6
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +9 -6
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +9 -6
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +9 -6
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +9 -6
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +9 -6
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +9 -6
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +9 -6
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +9 -6
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +13 -10
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +9 -6
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +9 -6
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +9 -6
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +9 -6
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +9 -6
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +9 -6
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +9 -6
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +9 -6
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +9 -6
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +9 -6
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +9 -6
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +9 -6
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +9 -6
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +9 -6
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +9 -6
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +9 -6
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +9 -6
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +9 -6
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +9 -6
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +9 -6
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +9 -6
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +9 -6
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +9 -6
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +9 -6
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +9 -6
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +9 -6
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +9 -6
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +9 -6
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +9 -6
  337. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +9 -6
  338. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +9 -6
  339. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +9 -6
  340. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +9 -6
  341. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -8
  342. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -8
  343. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +41 -41
  344. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +9 -6
  345. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +197 -54
  346. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +9 -6
  347. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +9 -6
  348. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +9 -6
  349. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +9 -6
  350. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +9 -6
  351. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +9 -6
  352. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +9 -6
  353. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +9 -6
  354. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +9 -6
  355. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +9 -6
  356. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +9 -6
  357. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +9 -6
  358. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +9 -6
  359. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +9 -6
  360. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +9 -6
  361. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +9 -6
  362. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +9 -6
  363. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +9 -6
  364. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +9 -6
  365. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +9 -6
  366. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +9 -6
  367. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +9 -6
  368. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +9 -6
  369. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +9 -6
  370. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +9 -6
  371. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +9 -6
  372. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +9 -6
  373. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +9 -6
  374. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +9 -6
  375. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +9 -6
  376. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +13 -10
  377. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +9 -6
  378. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +9 -6
  379. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +9 -6
  380. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +9 -6
  381. nautobot/project-static/docs/user-guide/index.html +9 -6
  382. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +9 -6
  383. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +9 -6
  384. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +10 -7
  385. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +9 -6
  386. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +9 -6
  387. nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -8
  388. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +9 -6
  389. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +9 -6
  390. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +9 -6
  391. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +9 -6
  392. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +9 -6
  393. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +9 -6
  394. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +9 -6
  395. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +9 -6
  396. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +9 -6
  397. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +9 -6
  398. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +9 -6
  399. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +9 -6
  400. nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +9 -6
  401. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +9 -6
  402. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +9 -6
  403. nautobot/project-static/docs/user-guide/platform-functionality/note.html +9 -6
  404. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +12 -9
  405. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +9 -6
  406. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +9 -6
  407. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +9 -6
  408. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +9 -6
  409. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +9 -6
  410. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +9 -6
  411. nautobot/project-static/docs/user-guide/platform-functionality/role.html +9 -6
  412. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9 -6
  413. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -8
  414. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +9 -6
  415. nautobot/project-static/docs/user-guide/platform-functionality/status.html +9 -6
  416. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +9 -6
  417. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +9 -6
  418. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +9 -6
  419. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +9 -6
  420. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +9 -6
  421. nautobot/project-static/fonts/UFL.txt +96 -96
  422. nautobot/project-static/img/nautobot_icon.svg +32 -34
  423. nautobot/project-static/js/forms.js +35 -2
  424. nautobot/project-static/js/table_sorting_indicator.js +0 -2
  425. nautobot/virtualization/filters.py +7 -0
  426. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/METADATA +8 -8
  427. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/RECORD +431 -421
  428. nautobot/core/templates/inc/breadcrumbs.html +0 -14
  429. nautobot/project-static/docs/assets/javascripts/bundle.50899def.min.js +0 -16
  430. nautobot/project-static/docs/requirements.txt +0 -14
  431. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/LICENSE.txt +0 -0
  432. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/NOTICE +0 -0
  433. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/WHEEL +0 -0
  434. {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,4 @@
1
+ from collections.abc import Iterable
1
2
  import datetime
2
3
  import json
3
4
  import logging
@@ -23,7 +24,7 @@ import yaml
23
24
  from nautobot.apps.config import get_app_settings_or_config
24
25
  from nautobot.core import forms
25
26
  from nautobot.core.constants import PAGINATE_COUNT_DEFAULT
26
- from nautobot.core.utils import color, config, data, logging as nautobot_logging, lookup
27
+ from nautobot.core.utils import color, config, data, deprecation, logging as nautobot_logging, lookup
27
28
  from nautobot.core.utils.requests import add_nautobot_version_query_param_to_url
28
29
 
29
30
  HTML_TRUE = mark_safe('<span class="text-success"><i class="mdi mdi-check-bold" title="Yes"></i></span>')
@@ -762,6 +763,23 @@ def render_address(address):
762
763
  return HTML_NONE
763
764
 
764
765
 
766
+ @register.filter()
767
+ def render_m2m(queryset, full_listing_link, verbose_name_plural, max_visible=5):
768
+ total_count = queryset.count()
769
+ display_count = min(total_count, max_visible)
770
+ if not display_count:
771
+ return HTML_NONE
772
+
773
+ items = [hyperlinked_object(record) for record in queryset[:display_count]]
774
+
775
+ remaining = total_count - display_count
776
+ if remaining > 0:
777
+ link = format_html('<a href="{}">... View {} more {}</a>', full_listing_link, remaining, verbose_name_plural)
778
+ items.append(link)
779
+
780
+ return format_html_join("", "<div>{}</div>", ((item,) for item in items)) if items else HTML_NONE
781
+
782
+
765
783
  @library.filter()
766
784
  @register.filter()
767
785
  def render_button_class(value):
@@ -823,11 +841,7 @@ def get_attr(obj, attr, default=None):
823
841
  return getattr(obj, attr, default)
824
842
 
825
843
 
826
- @register.simple_tag()
827
- def querystring(request, **kwargs):
828
- """
829
- Append or update the page number in a querystring.
830
- """
844
+ def _base_querystring(request, **kwargs):
831
845
  querydict = request.GET.copy()
832
846
  for k, v in kwargs.items():
833
847
  if v is not None:
@@ -841,6 +855,66 @@ def querystring(request, **kwargs):
841
855
  return ""
842
856
 
843
857
 
858
+ # TODO: Remove this tag in Nautobot 3.0.
859
+
860
+
861
+ @register.simple_tag()
862
+ @deprecation.method_deprecated(
863
+ "Leverage `legacy_querystring` instead of `querystring` if this templatetag is required. In Nautobot 3.0, "
864
+ "`querystring` will be removed in preparation for Django 5.2 in which there is a built-in querystring tag "
865
+ "that operates differently. You may find that `django_querystring` is more appropriate for your use case "
866
+ "and is a replica of Django 5.2's `querystring` templatetag."
867
+ )
868
+ def querystring(request, **kwargs):
869
+ return _base_querystring(request, **kwargs)
870
+
871
+
872
+ @register.simple_tag()
873
+ def legacy_querystring(request, **kwargs):
874
+ return _base_querystring(request, **kwargs)
875
+
876
+
877
+ # Note: This is vendored from Django 5.2
878
+ @register.simple_tag(name="django_querystring", takes_context=True)
879
+ def django_querystring(context, query_dict=None, **kwargs):
880
+ """
881
+ Add, remove, and change parameters of a ``QueryDict`` and return the result
882
+ as a query string. If the ``query_dict`` argument is not provided, default
883
+ to ``request.GET``.
884
+
885
+ For example::
886
+
887
+ {% django_querystring foo=3 %}
888
+
889
+ To remove a key::
890
+
891
+ {% django_querystring foo=None %}
892
+
893
+ To use with pagination::
894
+
895
+ {% django_querystring page=page_obj.next_page_number %}
896
+
897
+ A custom ``QueryDict`` can also be used::
898
+
899
+ {% django_querystring my_query_dict foo=3 %}
900
+ """
901
+ if query_dict is None:
902
+ query_dict = context.request.GET
903
+ params = query_dict.copy()
904
+ for key, value in kwargs.items():
905
+ if value is None:
906
+ if key in params:
907
+ del params[key]
908
+ elif isinstance(value, Iterable) and not isinstance(value, str):
909
+ params.setlist(key, value)
910
+ else:
911
+ params[key] = value
912
+ if not params and not query_dict:
913
+ return ""
914
+ query_string = params.urlencode()
915
+ return f"?{query_string}"
916
+
917
+
844
918
  @register.simple_tag()
845
919
  def table_config_button(table, table_name=None, extra_classes="", disabled=False):
846
920
  if table_name is None:
@@ -1278,3 +1352,27 @@ def saved_view_title(context, mode: Literal["html", "plain"] = "html"):
1278
1352
  return strip_tags(title)
1279
1353
 
1280
1354
  return title
1355
+
1356
+
1357
+ # https://www.djangosnippets.org/snippets/545/
1358
+ @register.tag(name="captureas")
1359
+ def do_captureas(parser, token):
1360
+ try:
1361
+ _, args = token.contents.split(None, 1)
1362
+ except ValueError:
1363
+ raise template.TemplateSyntaxError("'captureas' node requires a variable name.")
1364
+ nodelist = parser.parse(("endcaptureas",))
1365
+ parser.delete_first_token()
1366
+ return CaptureasNode(nodelist, args)
1367
+
1368
+
1369
+ class CaptureasNode(template.Node):
1370
+ def __init__(self, nodelist, varname):
1371
+ self.nodelist = nodelist
1372
+ self.varname = varname
1373
+
1374
+ def render(self, context):
1375
+ output = self.nodelist.render(context)
1376
+ output = output.strip()
1377
+ context[self.varname] = output
1378
+ return ""
@@ -1,10 +1,12 @@
1
+ from functools import partial
1
2
  import logging
2
3
 
3
4
  from django import template
4
- from django.utils.html import format_html_join
5
+ from django.utils.html import format_html_join, strip_spaces_between_tags
5
6
 
6
7
  from nautobot.core.ui.breadcrumbs import Breadcrumbs
7
8
  from nautobot.core.ui.titles import Titles
9
+ from nautobot.core.ui.utils import render_component_template
8
10
  from nautobot.core.utils.lookup import get_view_for_model
9
11
  from nautobot.core.views.utils import get_obj_from_context
10
12
 
@@ -31,21 +33,54 @@ def render_components(context, components):
31
33
 
32
34
  @register.simple_tag(takes_context=True)
33
35
  def render_title(context, mode="plain"):
36
+ """
37
+ Render the title passed in the context. Due to backwards compatibility in most of the Generic views,
38
+ we're either passing `title` to the template or render `title` defined in `view_titles`.
39
+
40
+ But in some newer views we want to have simple way to render title, only by defining `view_titles` within a view class.
41
+ """
42
+ if title := context.get("title"):
43
+ return title
44
+
34
45
  title_obj = context.get("view_titles")
35
46
  if title_obj is not None and isinstance(title_obj, Titles):
36
47
  return title_obj.render(context, mode=mode)
37
48
 
38
- if fallback_title := context.get("title"):
39
- return fallback_title
40
49
  return ""
41
50
 
42
51
 
43
52
  @register.simple_tag(takes_context=True)
44
- def render_breadcrumbs(context):
53
+ def render_breadcrumbs(context, legacy_default_breadcrumbs=None, legacy_block_breadcrumbs=None):
54
+ """
55
+ Renders the breadcrumbs using the UI Component Framework or legacy template-defined breadcrumbs.
56
+
57
+ Function checks if breadcrumbs from UI Component Framework are available and render them but only
58
+ when there is no other changes coming from legacy template-defined breadcrumbs.
59
+
60
+ Examples:
61
+ - UI Component Framework breadcrumbs are defined in the view. But in the template, {% block breadcrumbs %} is being used,
62
+ to override breadcrumbs or `{% block extra_breadcrumbs %}`. Output: template breadcrumbs will be rendered.
63
+ - There is no UI Component Framework breadcrumbs and no other block overrides. Output: default breadcrumbs will be rendered.
64
+ - UI Component Framework breadcrumbs are defined in the view. No breadcrumbs block overrides. Output: UI Component Framework breadcrumbs will be rendered.
65
+ """
66
+ render_template = partial(
67
+ render_component_template,
68
+ "components/breadcrumbs.html",
69
+ context,
70
+ )
71
+
72
+ if (
73
+ legacy_block_breadcrumbs
74
+ and strip_spaces_between_tags(legacy_default_breadcrumbs).strip()
75
+ != strip_spaces_between_tags(legacy_block_breadcrumbs).strip()
76
+ ):
77
+ return render_template(legacy_breadcrumbs=legacy_block_breadcrumbs)
78
+
45
79
  breadcrumbs_obj = context.get("breadcrumbs")
46
80
  if breadcrumbs_obj is not None and isinstance(breadcrumbs_obj, Breadcrumbs):
47
81
  return breadcrumbs_obj.render(context)
48
- return ""
82
+
83
+ return render_template(legacy_breadcrumbs=legacy_default_breadcrumbs)
49
84
 
50
85
 
51
86
  @register.simple_tag(takes_context=True)
@@ -99,6 +99,17 @@ class FilterTestCases:
99
99
  self.assertIsNotNone(self.filterset)
100
100
  return self.filterset.declared_filters["q"].filter_predicates
101
101
 
102
+ def test_no_distinct_on_empty_filter_params(self):
103
+ """Verify that an empty filterset doesn't cause a `SELECT DISTINCT`."""
104
+ self.assertIsNotNone(self.filterset)
105
+ filterset = self.filterset({}, self.queryset) # pylint: disable=not-callable # see assertion above
106
+ self.assertTrue(filterset.is_valid())
107
+ self.assertNotIn(
108
+ "SELECT DISTINCT",
109
+ str(filterset.qs.query),
110
+ "Filter set with empty parameter added `DISTINCT` to select query. This needs to be avoided because it incurs heavy performance penalties.",
111
+ )
112
+
102
113
  def test_id(self):
103
114
  """Verify that the filterset supports filtering by id with only lookup `__n`."""
104
115
  self.assertIsNotNone(self.filterset)
@@ -326,29 +337,34 @@ class FilterTestCases:
326
337
  if not isinstance(obj_field, (CharField, TextField)):
327
338
  self.skipTest("Not a CharField or TextField")
328
339
 
340
+ original_value = getattr(obj, obj_field_name)
329
341
  # Create random lowercase string to use for icontains lookup
330
342
  max_length = obj_field.max_length or CHARFIELD_MAX_LENGTH
331
343
  randomized_attr_value = "".join(random.choices(string.ascii_lowercase, k=max_length)) # noqa: S311 # pseudo-random generator
332
- setattr(obj, obj_field_name, randomized_attr_value)
333
- obj.save()
334
-
335
- # if lookup_method is iexact use the full updated attr
336
- if lookup_method == "iexact":
337
- lookup = randomized_attr_value.upper()
338
- model_queryset = self.queryset.filter(**{f"{filter_field_name}__iexact": lookup})
339
- else:
340
- lookup = randomized_attr_value[1:].upper()
341
- model_queryset = self.queryset.filter(**{f"{filter_field_name}__icontains": lookup})
342
- params = {"q": lookup}
343
- filterset_result = self.filterset(params, self.queryset) # pylint: disable=not-callable
344
-
345
- self.assertTrue(filterset_result.is_valid())
346
- self.assertQuerysetEqualAndNotEmpty(
347
- filterset_result.qs,
348
- model_queryset,
349
- ordered=False,
350
- msg=lookup,
351
- )
344
+ try:
345
+ setattr(obj, obj_field_name, randomized_attr_value)
346
+ obj.save()
347
+
348
+ # if lookup_method is iexact use the full updated attr
349
+ if lookup_method == "iexact":
350
+ lookup = randomized_attr_value.upper()
351
+ model_queryset = self.queryset.filter(**{f"{filter_field_name}__iexact": lookup})
352
+ else:
353
+ lookup = randomized_attr_value[1:].upper()
354
+ model_queryset = self.queryset.filter(**{f"{filter_field_name}__icontains": lookup})
355
+ params = {"q": lookup}
356
+ filterset_result = self.filterset(params, self.queryset) # pylint: disable=not-callable
357
+
358
+ self.assertTrue(filterset_result.is_valid())
359
+ self.assertQuerysetEqualAndNotEmpty(
360
+ filterset_result.qs,
361
+ model_queryset,
362
+ ordered=False,
363
+ msg=lookup,
364
+ )
365
+ finally:
366
+ setattr(obj, obj_field_name, original_value)
367
+ obj.save()
352
368
 
353
369
  def _get_relevant_filterset_queryset(self, queryset, *filter_params):
354
370
  """Gets the relevant queryset based on filter parameters."""
@@ -454,7 +470,7 @@ class FilterTestCases:
454
470
  tenant_groups = list(
455
471
  models.TenantGroup.objects.filter(
456
472
  tenants__isnull=False, **{f"tenants__{self.tenancy_related_name}__isnull": False}
457
- )
473
+ ).distinct()
458
474
  )[:2]
459
475
  tenant_groups_including_children = []
460
476
  for tenant_group in tenant_groups:
@@ -156,7 +156,7 @@ class NautobotTestCaseMixin:
156
156
  """
157
157
  for name in names:
158
158
  ct, action = permissions.resolve_permission_ct(name)
159
- obj_perm = users_models.ObjectPermission(name=name, actions=[action], **kwargs)
159
+ obj_perm, _ = users_models.ObjectPermission.objects.get_or_create(name=name, actions=[action], **kwargs)
160
160
  obj_perm.save()
161
161
  obj_perm.users.add(self.user)
162
162
  obj_perm.object_types.add(ct)
@@ -1,4 +1,5 @@
1
1
  import contextlib
2
+ import inspect
2
3
  import re
3
4
  from typing import Optional, Sequence
4
5
  from unittest import mock, skipIf
@@ -281,8 +282,7 @@ class ViewTestCases:
281
282
  if getattr(obj, "is_contact_associable_model", False):
282
283
  self.assertBodyContains(
283
284
  response,
284
- f'<a href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" aria-controls="contacts" role="tab" data-toggle="tab">Contacts</a>',
285
- html=True,
285
+ f'href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)"',
286
286
  )
287
287
  else:
288
288
  self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
@@ -304,8 +304,7 @@ class ViewTestCases:
304
304
  if getattr(obj, "is_contact_associable_model", False):
305
305
  self.assertBodyContains(
306
306
  response,
307
- f'<a href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)" aria-controls="contacts" role="tab" data-toggle="tab">Contacts</a>',
308
- html=True,
307
+ f'href="{obj.get_absolute_url()}#contacts" onclick="switch_tab(this.href)"',
309
308
  )
310
309
  else:
311
310
  self.assertNotContains(response, f"{obj.get_absolute_url()}#contacts")
@@ -778,6 +777,30 @@ class ViewTestCases:
778
777
  response_body = response.content.decode(response.charset)
779
778
  self.assertNotIn('<i class="mdi mdi-circle-small"></i>', response_body)
780
779
 
780
+ def test_model_properties_as_table_columns_are_not_orderable(self):
781
+ """
782
+ Check for table columns that are property-based and not orderable.
783
+ """
784
+ table_class = getattr(self.get_list_view(), "table_class", None)
785
+ if not table_class:
786
+ return
787
+
788
+ queryset = self._get_queryset()
789
+ table = table_class(queryset)
790
+ model_cls = table._meta.model
791
+
792
+ property_fields = {name for name, _ in inspect.getmembers(model_cls, lambda o: isinstance(o, property))}
793
+
794
+ for name, column in table.base_columns.items():
795
+ if hasattr(column, "order_by") and column.order_by:
796
+ continue
797
+ if name in property_fields and name != "pk":
798
+ with self.subTest(column_name=name):
799
+ self.assertFalse(
800
+ column.orderable,
801
+ f"On Table `{table_class.__name__}` the property-based column `{name}` should be orderable=False or use a custom order_by",
802
+ )
803
+
781
804
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
782
805
  def test_list_objects_anonymous(self):
783
806
  # Make the request as an unauthenticated user
@@ -18,8 +18,18 @@ class TableTestCase(TestCase):
18
18
  def _validate_sorted_tree_queryset_same_with_table_queryset(self, queryset, table_class, field_name):
19
19
  with self.subTest(f"Assert sorting {table_class.__name__} on '{field_name}'"):
20
20
  table = table_class(queryset.with_tree_fields(), order_by=field_name)
21
- table_queryset_data = table.data.data.values_list("pk", flat=True)
22
- sorted_queryset = queryset.with_tree_fields().extra(order_by=[field_name]).values_list("pk", flat=True)
21
+ table_queryset_data = table.data.data.values_list(field_name, flat=True)
22
+ sorted_queryset = (
23
+ queryset.with_tree_fields().extra(order_by=[field_name]).values_list(field_name, flat=True)
24
+ )
25
+ self.assertEqual(list(table_queryset_data), list(sorted_queryset))
26
+
27
+ with self.subTest(f"Assert sorting {table_class.__name__} on '-{field_name}'"):
28
+ table = table_class(queryset.with_tree_fields(), order_by=f"-{field_name}")
29
+ table_queryset_data = table.data.data.values_list(field_name, flat=True)
30
+ sorted_queryset = (
31
+ queryset.with_tree_fields().extra(order_by=[f"-{field_name}"]).values_list(field_name, flat=True)
32
+ )
23
33
  self.assertEqual(list(table_queryset_data), list(sorted_queryset))
24
34
 
25
35
  def test_tree_model_table_orderable(self):
@@ -61,14 +71,41 @@ class TableTestCase(TestCase):
61
71
  table_avail_fields = set(model_field_names) & set(table_class.Meta.fields)
62
72
  for table_field_name in table_avail_fields:
63
73
  self._validate_sorted_tree_queryset_same_with_table_queryset(queryset, table_class, table_field_name)
64
- self._validate_sorted_tree_queryset_same_with_table_queryset(
65
- queryset, table_class, f"-{table_field_name}"
66
- )
67
74
 
68
75
  # Test for `rack_count`
69
76
  queryset = RackGroupTable.Meta.model.objects.annotate(rack_count=count_related(Rack, "rack_group")).all()
70
77
  self._validate_sorted_tree_queryset_same_with_table_queryset(queryset, RackGroupTable, "rack_count")
71
- self._validate_sorted_tree_queryset_same_with_table_queryset(queryset, RackGroupTable, "-rack_count")
78
+
79
+ # https://github.com/nautobot/nautobot/issues/7330 - sorting by custom field
80
+ l1 = Location.objects.first()
81
+ l1._custom_field_data["example_app_auto_custom_field"] = "alpha"
82
+ l1.validated_save()
83
+ l2 = Location.objects.last()
84
+ l2._custom_field_data["example_app_auto_custom_field"] = "omega"
85
+ l2.validated_save()
86
+ table = LocationTable(
87
+ Location.objects.exclude(_custom_field_data__example_app_auto_custom_field="Default value")
88
+ )
89
+
90
+ table.order_by = ["cf_example_app_auto_custom_field"]
91
+ table_queryset_data = table.data.data.values_list("pk", flat=True)
92
+ sorted_queryset = (
93
+ Location.objects.with_tree_fields()
94
+ .exclude(_custom_field_data__example_app_auto_custom_field="Default value")
95
+ .extra(order_by=["_custom_field_data__example_app_auto_custom_field"])
96
+ .values_list("pk", flat=True)
97
+ )
98
+ self.assertEqual(list(table_queryset_data), list(sorted_queryset))
99
+
100
+ table.order_by = ["-cf_example_app_auto_custom_field"]
101
+ table_queryset_data = table.data.data.values_list("pk", flat=True)
102
+ sorted_queryset = (
103
+ Location.objects.with_tree_fields()
104
+ .exclude(_custom_field_data__example_app_auto_custom_field="Default value")
105
+ .extra(order_by=["-_custom_field_data__example_app_auto_custom_field"])
106
+ .values_list("pk", flat=True)
107
+ )
108
+ self.assertEqual(list(table_queryset_data), list(sorted_queryset))
72
109
 
73
110
  def test_base_table_apis(self):
74
111
  """
@@ -0,0 +1,146 @@
1
+ from django.template import Context
2
+
3
+ from nautobot.core.templatetags import ui_framework
4
+ from nautobot.core.testing import TestCase
5
+ from nautobot.core.ui.breadcrumbs import Breadcrumbs
6
+ from nautobot.core.ui.titles import Titles
7
+
8
+
9
+ class NautobotTemplatetagsUIComponentsTest(TestCase):
10
+ """Tests template tags from ui_framework module."""
11
+
12
+ # ---------------------------
13
+ # render_title
14
+ # ---------------------------
15
+
16
+ def test_render_title_with_legacy_title_present(self):
17
+ context = Context(
18
+ {
19
+ "title": "Custom Title",
20
+ "view_titles": Titles(),
21
+ "verbose_name_plural": "Default Title",
22
+ }
23
+ )
24
+ output = ui_framework.render_title(context)
25
+
26
+ self.assertEqual(output, "Custom Title")
27
+
28
+ def test_render_title_with_view_titles_only(self):
29
+ context = Context(
30
+ {
31
+ "view_titles": Titles(),
32
+ "verbose_name_plural": "Default Title",
33
+ }
34
+ )
35
+ output = ui_framework.render_title(context)
36
+
37
+ self.assertEqual(output, "Default Title")
38
+
39
+ def test_render_title_with_invalid_view_titles(self):
40
+ class MyStuff:
41
+ pass
42
+
43
+ context = Context(
44
+ {
45
+ "view_titles": MyStuff(),
46
+ "verbose_name_plural": "Default Title",
47
+ }
48
+ )
49
+ output = ui_framework.render_title(context)
50
+
51
+ self.assertEqual(output, "")
52
+
53
+ def test_render_title_with_empty_context(self):
54
+ context = Context({})
55
+ output = ui_framework.render_title(context)
56
+
57
+ self.assertEqual(output, "")
58
+
59
+ # ---------------------------
60
+ # render_breadcrumbs
61
+ # ---------------------------
62
+
63
+ def test_render_breadcrumbs(self):
64
+ context = Context(
65
+ {
66
+ "list_url": "home",
67
+ "title": "New Home",
68
+ "view_action": "list",
69
+ "breadcrumbs": Breadcrumbs(),
70
+ }
71
+ )
72
+ output = ui_framework.render_breadcrumbs(context)
73
+
74
+ self.assertHTMLEqual(output, '<ol class="breadcrumb"><li><a href="/">New Home</a></li></ol>')
75
+
76
+ def test_render_breadcrumbs_empty_context(self):
77
+ context = Context({})
78
+ output = ui_framework.render_breadcrumbs(context)
79
+
80
+ self.assertHTMLEqual(output, '<ol class="breadcrumb"></ol>')
81
+
82
+ def test_render_breadcrumbs_with_legacy_breadcrumbs(self):
83
+ legacy_breadcrumbs = '<li><a href="/">Home</a></li>'
84
+ context = Context({})
85
+ output = ui_framework.render_breadcrumbs(context, legacy_breadcrumbs)
86
+
87
+ self.assertHTMLEqual(output, '<ol class="breadcrumb"><li><a href="/">Home</a></li></ol>')
88
+
89
+ def test_render_breadcrumbs_with_legacy_and_block_breadcrumbs_the_same_with_breadcrumbs_class(self):
90
+ legacy_breadcrumbs = block_breadcrumbs = '<li><a href="/">Home</a></li>'
91
+ context = Context(
92
+ {
93
+ "list_url": "home",
94
+ "title": "New Home",
95
+ "view_action": "list",
96
+ "breadcrumbs": Breadcrumbs(),
97
+ }
98
+ )
99
+ output = ui_framework.render_breadcrumbs(context, legacy_breadcrumbs, block_breadcrumbs)
100
+
101
+ self.assertHTMLEqual(output, '<ol class="breadcrumb"><li><a href="/">New Home</a></li></ol>')
102
+
103
+ def test_render_breadcrumbs_with_legacy_and_block_breadcrumbs_the_same_and_no_breadcrumbs_class(self):
104
+ legacy_breadcrumbs = block_breadcrumbs = '<li><a href="/">Home</a></li>'
105
+ context = Context({})
106
+ output = ui_framework.render_breadcrumbs(context, legacy_breadcrumbs, block_breadcrumbs)
107
+
108
+ self.assertHTMLEqual(output, '<ol class="breadcrumb"><li><a href="/">Home</a></li></ol>')
109
+
110
+ def test_render_breadcrumbs_with_legacy_breadcrumbs_override(self):
111
+ legacy_breadcrumbs = '<li><a href="/">Home</a></li>'
112
+ block_breadcrumbs = '<li><a href="/">Override</a></li>'
113
+ context = Context(
114
+ {
115
+ "list_url": "home",
116
+ "title": "New Home",
117
+ "view_action": "list",
118
+ "breadcrumbs": Breadcrumbs(),
119
+ }
120
+ )
121
+ output = ui_framework.render_breadcrumbs(context, legacy_breadcrumbs, block_breadcrumbs)
122
+
123
+ self.assertHTMLEqual(output, '<ol class="breadcrumb"><li><a href="/">Override</a></li></ol>')
124
+
125
+ def test_render_breadcrumbs_strips_tags(self):
126
+ legacy_breadcrumbs = """
127
+ <li>
128
+ <a href="/">Home</a>
129
+ </li>"""
130
+
131
+ block_breadcrumbs = """<li>
132
+
133
+ <a href="/">Home</a></li>
134
+ """
135
+
136
+ context = Context(
137
+ {
138
+ "list_url": "home",
139
+ "title": "New Home",
140
+ "view_action": "list",
141
+ "breadcrumbs": Breadcrumbs(),
142
+ }
143
+ )
144
+ output = ui_framework.render_breadcrumbs(context, legacy_breadcrumbs, block_breadcrumbs)
145
+
146
+ self.assertHTMLEqual(output, '<ol class="breadcrumb"><li><a href="/">New Home</a></li></ol>')
@@ -163,7 +163,7 @@ class TitlesTestCase(TestCase):
163
163
  """Test rendering when no view_action is provided."""
164
164
  context = Context({"verbose_name_plural": "devices"})
165
165
  result = self.titles.render(context)
166
- self.assertEqual(result, "Devices") # Should use list_action as default
166
+ self.assertEqual(result, "Devices") # Should use * action as default
167
167
 
168
168
  def test_get_extra_context(self):
169
169
  """Test that get_extra_context returns empty dict by default."""
@@ -173,7 +173,7 @@ class TitlesTestCase(TestCase):
173
173
 
174
174
  def test_get_extra_context_is_used_during_render(self):
175
175
  """Test that get_extra_context is being used to extend the context."""
176
- context = Context({})
176
+ context = Context({"view_action": "list"})
177
177
 
178
178
  class TitlesSubClass(Titles):
179
179
  def get_extra_context(self, context: Context) -> dict:
@@ -7,7 +7,7 @@ from django.test import RequestFactory
7
7
 
8
8
  from nautobot.core.templatetags.helpers import HTML_NONE
9
9
  from nautobot.core.testing import TestCase
10
- from nautobot.core.ui.object_detail import BaseTextPanel, DataTablePanel, ObjectsTablePanel, Panel
10
+ from nautobot.core.ui.object_detail import BaseTextPanel, DataTablePanel, ObjectFieldsPanel, ObjectsTablePanel, Panel
11
11
  from nautobot.dcim.models import DeviceRedundancyGroup
12
12
  from nautobot.dcim.tables.devices import DeviceTable
13
13
 
@@ -68,6 +68,19 @@ class DataTablePanelTest(TestCase):
68
68
  )
69
69
 
70
70
 
71
+ class ObjectFieldsPanelTest(TestCase):
72
+ def test_get_data_ignore_nonexistent_fields(self):
73
+ panel = ObjectFieldsPanel(weight=100, fields=["name", "foo", "bar"], ignore_nonexistent_fields=True)
74
+ redundancy_group = DeviceRedundancyGroup.objects.first()
75
+ context = Context({"object": redundancy_group})
76
+ data = panel.get_data(context)
77
+ self.assertEqual(data, {"name": redundancy_group.name}) # no keys for nonexistent fields
78
+
79
+ panel = ObjectFieldsPanel(weight=100, fields=["name", "foo", "bar"], ignore_nonexistent_fields=False)
80
+ with self.assertRaises(AttributeError):
81
+ data = panel.get_data(context)
82
+
83
+
71
84
  class BaseTextPanelTest(TestCase):
72
85
  def test_init_set_object_params(self):
73
86
  # Test default settings