nautobot 2.2.4__py3-none-any.whl → 2.2.5__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 (303) hide show
  1. nautobot/circuits/forms.py +15 -0
  2. nautobot/circuits/navigation.py +9 -1
  3. nautobot/circuits/views.py +2 -0
  4. nautobot/core/filters.py +11 -0
  5. nautobot/core/settings.yaml +3 -3
  6. nautobot/core/testing/filters.py +24 -1
  7. nautobot/core/testing/views.py +13 -1
  8. nautobot/core/views/utils.py +18 -1
  9. nautobot/dcim/filters/__init__.py +1 -1
  10. nautobot/dcim/forms.py +23 -4
  11. nautobot/dcim/tables/devicetypes.py +15 -4
  12. nautobot/dcim/tests/test_views.py +84 -0
  13. nautobot/dcim/views.py +3 -0
  14. nautobot/extras/api/views.py +2 -2
  15. nautobot/extras/context_managers.py +3 -0
  16. nautobot/extras/filters/__init__.py +15 -1
  17. nautobot/extras/forms/forms.py +33 -0
  18. nautobot/extras/forms/mixins.py +0 -6
  19. nautobot/extras/signals.py +6 -1
  20. nautobot/extras/tests/test_api.py +24 -2
  21. nautobot/extras/tests/test_context_managers.py +33 -1
  22. nautobot/extras/tests/test_filters.py +69 -0
  23. nautobot/extras/tests/test_forms.py +0 -3
  24. nautobot/extras/tests/test_views.py +48 -4
  25. nautobot/extras/views.py +12 -10
  26. nautobot/ipam/forms.py +18 -0
  27. nautobot/ipam/tests/test_views.py +9 -2
  28. nautobot/ipam/views.py +11 -0
  29. nautobot/project-static/docs/404.html +3 -3
  30. nautobot/project-static/docs/apps/index.html +3 -3
  31. nautobot/project-static/docs/apps/nautobot-apps.html +3 -3
  32. nautobot/project-static/docs/assets/javascripts/{bundle.3220b9d7.min.js → bundle.ebd0bdb7.min.js} +5 -5
  33. nautobot/project-static/docs/assets/javascripts/{bundle.3220b9d7.min.js.map → bundle.ebd0bdb7.min.js.map} +3 -3
  34. nautobot/project-static/docs/assets/stylesheets/{main.66ac8b77.min.css → main.6543a935.min.css} +1 -1
  35. nautobot/project-static/docs/assets/stylesheets/{main.66ac8b77.min.css.map → main.6543a935.min.css.map} +1 -1
  36. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +3 -3
  37. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +3 -3
  38. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3 -3
  39. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +3 -3
  40. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +3 -3
  41. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +3 -3
  42. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +3 -3
  43. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +3 -3
  44. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +3 -3
  45. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +3 -3
  46. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +3 -3
  47. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +3 -3
  48. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
  49. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
  50. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
  51. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
  52. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
  53. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
  54. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +3 -3
  55. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +3 -3
  56. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +3 -3
  57. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +3 -3
  58. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3 -3
  59. nautobot/project-static/docs/development/apps/api/configuration-view.html +3 -3
  60. nautobot/project-static/docs/development/apps/api/database-backend-config.html +3 -3
  61. nautobot/project-static/docs/development/apps/api/models/django-admin.html +3 -3
  62. nautobot/project-static/docs/development/apps/api/models/global-search.html +3 -3
  63. nautobot/project-static/docs/development/apps/api/models/graphql.html +3 -3
  64. nautobot/project-static/docs/development/apps/api/models/index.html +3 -3
  65. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
  66. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
  67. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +4 -4
  68. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
  69. nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
  70. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
  71. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
  72. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
  73. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
  74. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
  75. nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
  76. nautobot/project-static/docs/development/apps/api/setup.html +3 -3
  77. nautobot/project-static/docs/development/apps/api/testing.html +3 -3
  78. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
  79. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
  80. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
  81. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
  82. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
  83. nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
  84. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
  85. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
  86. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
  87. nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
  88. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
  89. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
  90. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
  91. nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
  92. nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
  93. nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
  94. nautobot/project-static/docs/development/apps/index.html +3 -3
  95. nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
  96. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
  97. nautobot/project-static/docs/development/apps/migration/from-v1.html +5 -5
  98. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
  99. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
  100. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
  101. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
  102. nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
  103. nautobot/project-static/docs/development/core/application-registry.html +4 -4
  104. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  105. nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
  106. nautobot/project-static/docs/development/core/caching.html +3 -3
  107. nautobot/project-static/docs/development/core/controllers.html +3 -3
  108. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
  109. nautobot/project-static/docs/development/core/generic-views.html +3 -3
  110. nautobot/project-static/docs/development/core/getting-started.html +5 -5
  111. nautobot/project-static/docs/development/core/homepage.html +3 -3
  112. nautobot/project-static/docs/development/core/index.html +3 -3
  113. nautobot/project-static/docs/development/core/model-checklist.html +3 -3
  114. nautobot/project-static/docs/development/core/model-features.html +3 -3
  115. nautobot/project-static/docs/development/core/natural-keys.html +3 -3
  116. nautobot/project-static/docs/development/core/navigation-menu.html +3 -3
  117. nautobot/project-static/docs/development/core/release-checklist.html +3 -3
  118. nautobot/project-static/docs/development/core/role-internals.html +3 -3
  119. nautobot/project-static/docs/development/core/settings.html +3 -3
  120. nautobot/project-static/docs/development/core/style-guide.html +3 -3
  121. nautobot/project-static/docs/development/core/templates.html +3 -3
  122. nautobot/project-static/docs/development/core/testing.html +4 -4
  123. nautobot/project-static/docs/development/core/user-preferences.html +3 -3
  124. nautobot/project-static/docs/development/index.html +3 -3
  125. nautobot/project-static/docs/development/jobs/index.html +3 -3
  126. nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -3
  127. nautobot/project-static/docs/index.html +3 -3
  128. nautobot/project-static/docs/release-notes/index.html +3 -3
  129. nautobot/project-static/docs/release-notes/version-1.0.html +4 -4
  130. nautobot/project-static/docs/release-notes/version-1.1.html +3 -3
  131. nautobot/project-static/docs/release-notes/version-1.2.html +5 -5
  132. nautobot/project-static/docs/release-notes/version-1.3.html +4 -4
  133. nautobot/project-static/docs/release-notes/version-1.4.html +5 -5
  134. nautobot/project-static/docs/release-notes/version-1.5.html +5 -5
  135. nautobot/project-static/docs/release-notes/version-1.6.html +543 -163
  136. nautobot/project-static/docs/release-notes/version-2.0.html +6 -6
  137. nautobot/project-static/docs/release-notes/version-2.1.html +3 -3
  138. nautobot/project-static/docs/release-notes/version-2.2.html +263 -84
  139. nautobot/project-static/docs/requirements.txt +2 -2
  140. nautobot/project-static/docs/search/search_index.json +1 -1
  141. nautobot/project-static/docs/sitemap.xml +254 -254
  142. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  143. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
  144. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +3 -3
  145. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +5 -5
  146. nautobot/project-static/docs/user-guide/administration/configuration/index.html +4 -4
  147. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +6 -6
  148. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +3 -3
  149. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +3 -3
  150. nautobot/project-static/docs/user-guide/administration/guides/caching.html +3 -3
  151. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
  152. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +3 -3
  153. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +3 -3
  154. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +3 -3
  155. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +4 -4
  156. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +3 -3
  157. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +3 -3
  158. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +3 -3
  159. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +3 -3
  160. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -3
  161. nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
  162. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +3 -3
  163. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +7 -7
  164. nautobot/project-static/docs/user-guide/administration/installation/services.html +4 -4
  165. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +3 -3
  166. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +3 -3
  167. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +3 -3
  168. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +3 -3
  169. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  170. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +3 -3
  171. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +3 -3
  172. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +3 -3
  173. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +3 -3
  174. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +3 -3
  175. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +4 -4
  176. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +3 -3
  177. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +3 -3
  178. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +3 -22
  179. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +5 -5
  180. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +3 -3
  181. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +3 -3
  182. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +3 -3
  183. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +3 -3
  184. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +3 -3
  185. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +3 -3
  186. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +3 -3
  187. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +3 -3
  188. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +3 -3
  189. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +3 -3
  190. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +3 -3
  191. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +3 -3
  192. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +3 -3
  193. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +5 -5
  194. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +3 -3
  195. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +3 -3
  196. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +3 -3
  197. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +3 -3
  198. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +4 -4
  199. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +3 -3
  200. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +3 -3
  201. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +3 -3
  202. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +3 -3
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +3 -3
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +3 -3
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +3 -3
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +3 -3
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +3 -3
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +3 -3
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +3 -3
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +3 -3
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +3 -3
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +3 -3
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +3 -3
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +3 -3
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +3 -3
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +3 -3
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +3 -3
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +3 -3
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +3 -3
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +3 -3
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +3 -3
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +3 -3
  223. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +3 -3
  224. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +3 -3
  225. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +3 -3
  226. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +3 -3
  227. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +3 -3
  228. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +3 -3
  229. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +3 -3
  230. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +3 -3
  231. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +3 -3
  232. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +3 -3
  233. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +3 -3
  234. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +3 -3
  235. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
  236. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
  237. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
  238. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
  239. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
  240. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
  241. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
  242. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
  243. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
  244. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
  245. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  246. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +6 -6
  247. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
  248. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
  249. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +4 -4
  250. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
  251. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
  252. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
  253. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
  254. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +4 -4
  255. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
  256. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
  257. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
  258. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
  259. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
  260. nautobot/project-static/docs/user-guide/index.html +3 -3
  261. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
  262. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
  263. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
  264. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
  265. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
  266. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
  267. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
  268. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
  269. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
  270. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
  271. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
  272. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
  273. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +4 -4
  274. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
  275. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
  276. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +5 -5
  277. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +5 -5
  278. nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
  279. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +3 -3
  280. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +3 -3
  281. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +3 -3
  282. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +3 -3
  283. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +3 -3
  284. nautobot/project-static/docs/user-guide/platform-functionality/role.html +4 -36
  285. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
  286. nautobot/project-static/docs/user-guide/platform-functionality/status.html +3 -3
  287. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +3 -3
  288. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +3 -3
  289. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +3 -3
  290. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +3 -3
  291. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +3 -3
  292. nautobot/project-static/js/forms.js +2 -1
  293. nautobot/tenancy/forms.py +9 -0
  294. nautobot/tenancy/views.py +1 -0
  295. nautobot/virtualization/forms.py +18 -6
  296. nautobot/virtualization/templates/virtualization/clustertype.html +2 -2
  297. nautobot/virtualization/views.py +2 -0
  298. {nautobot-2.2.4.dist-info → nautobot-2.2.5.dist-info}/METADATA +1 -1
  299. {nautobot-2.2.4.dist-info → nautobot-2.2.5.dist-info}/RECORD +303 -303
  300. {nautobot-2.2.4.dist-info → nautobot-2.2.5.dist-info}/LICENSE.txt +0 -0
  301. {nautobot-2.2.4.dist-info → nautobot-2.2.5.dist-info}/NOTICE +0 -0
  302. {nautobot-2.2.4.dist-info → nautobot-2.2.5.dist-info}/WHEEL +0 -0
  303. {nautobot-2.2.4.dist-info → nautobot-2.2.5.dist-info}/entry_points.txt +0 -0
@@ -145,6 +145,12 @@ class CircuitTypeForm(NautobotModelForm):
145
145
  ]
146
146
 
147
147
 
148
+ class CircuitTypeFilterForm(NautobotFilterForm):
149
+ model = CircuitType
150
+ q = forms.CharField(required=False, label="Search")
151
+ name = forms.CharField(required=False)
152
+
153
+
148
154
  #
149
155
  # Circuits
150
156
  #
@@ -262,3 +268,12 @@ class CircuitTerminationForm(LocatableModelFormMixin, NautobotModelForm):
262
268
  widgets = {
263
269
  "term_side": forms.HiddenInput(),
264
270
  }
271
+
272
+
273
+ class CircuitTerminationFilterForm(LocatableModelFilterFormMixin, NautobotFilterForm):
274
+ model = CircuitTermination
275
+ q = forms.CharField(required=False, label="Search")
276
+ circuit = DynamicModelMultipleChoiceField(queryset=Circuit.objects.all(), to_field_name="cid", required=False)
277
+ provider_network = DynamicModelMultipleChoiceField(
278
+ queryset=ProviderNetwork.objects.all(), to_field_name="name", required=False
279
+ )
@@ -33,10 +33,18 @@ menu_items = (
33
33
  ),
34
34
  ),
35
35
  ),
36
+ NavMenuItem(
37
+ link="circuits:circuittermination_list",
38
+ name="Circuit Terminations",
39
+ weight=200,
40
+ permissions=[
41
+ "circuits.view_circuittermination",
42
+ ],
43
+ ),
36
44
  NavMenuItem(
37
45
  link="circuits:circuittype_list",
38
46
  name="Circuit Types",
39
- weight=200,
47
+ weight=300,
40
48
  permissions=[
41
49
  "circuits.view_circuittype",
42
50
  ],
@@ -27,6 +27,7 @@ class CircuitTypeUIViewSet(
27
27
  view_mixins.ObjectNotesViewMixin,
28
28
  ):
29
29
  filterset_class = filters.CircuitTypeFilterSet
30
+ filterset_form_class = forms.CircuitTypeFilterForm
30
31
  form_class = forms.CircuitTypeForm
31
32
  queryset = CircuitType.objects.annotate(circuit_count=count_related(Circuit, "circuit_type"))
32
33
  serializer_class = serializers.CircuitTypeSerializer
@@ -67,6 +68,7 @@ class CircuitTerminationUIViewSet(
67
68
  ):
68
69
  action_buttons = ("import", "export")
69
70
  filterset_class = filters.CircuitTerminationFilterSet
71
+ filterset_form_class = forms.CircuitTerminationFilterForm
70
72
  form_class = forms.CircuitTerminationForm
71
73
  queryset = CircuitTermination.objects.all()
72
74
  serializer_class = serializers.CircuitTerminationSerializer
nautobot/core/filters.py CHANGED
@@ -176,6 +176,9 @@ class ContentTypeFilterMixin:
176
176
  if value in EMPTY_VALUES:
177
177
  return qs
178
178
 
179
+ if value.isdigit():
180
+ return qs.filter(**{f"{self.field_name}__pk": value})
181
+
179
182
  try:
180
183
  app_label, model = value.lower().split(".")
181
184
  except ValueError:
@@ -242,6 +245,9 @@ class ContentTypeMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
242
245
  if self.conjoined:
243
246
  qs = ContentTypeFilter.filter(self, qs, v)
244
247
  else:
248
+ if v.isdigit():
249
+ q |= models.Q(**{f"{self.field_name}__pk": value})
250
+ continue
245
251
  # Similar to the ContentTypeFilter.filter() call above, but instead of narrowing the query each time
246
252
  # (a AND b AND c ...) we broaden the query each time (a OR b OR c ...).
247
253
  # Specifically, we're mapping a value like ['dcim.device', 'ipam.vlan'] to a query like
@@ -716,6 +722,11 @@ class BaseFilterSet(django_filters.FilterSet):
716
722
  """
717
723
  filters = super().get_filters()
718
724
 
725
+ # Remove any filters that may have been auto-generated from private model attributes
726
+ for filter_name in list(filters.keys()):
727
+ if filter_name.startswith("_"):
728
+ del filters[filter_name]
729
+
719
730
  # django-filters has no concept of "abstract" filtersets, so we have to fake it
720
731
  if cls._meta.model is not None:
721
732
  new_filters = {}
@@ -773,7 +773,7 @@ properties:
773
773
  description: "A mapping of permissions to assign a new user account when created using SSO authentication."
774
774
  details: |-
775
775
  Each key in the dictionary will be the permission name specified as `<app_label>.<action>_<model>`,
776
- and the value should be set to the permission [constraints](../guides/permissions.md#constraints),
776
+ and the value should be set to the permission [constraints](../guides/permissions.md#example-constraint-definitions),
777
777
  or `None` to allow all objects.
778
778
 
779
779
  Example:
@@ -1278,7 +1278,7 @@ properties:
1278
1278
 
1279
1279
  !!! note
1280
1280
  If a given device has an appropriately populated
1281
- [secrets group](../../platform-functionality/secret.md#secretsgroup) assigned to it,
1281
+ [secrets group](../../platform-functionality/secret.md#secrets-groups) assigned to it,
1282
1282
  the [secrets](../../platform-functionality/secret.md) defined in that group will take precedence
1283
1283
  over these default values.
1284
1284
  environment_variable: "NAUTOBOT_NAPALM_PASSWORD"
@@ -1300,7 +1300,7 @@ properties:
1300
1300
 
1301
1301
  !!! note
1302
1302
  If a given device has an appropriately populated
1303
- [secrets group](../../platform-functionality/secret.md#secretsgroup) assigned to it,
1303
+ [secrets group](../../platform-functionality/secret.md#secrets-groups) assigned to it,
1304
1304
  the [secrets](../../platform-functionality/secret.md) defined in that group will take precedence
1305
1305
  over these default values.
1306
1306
  environment_variable: "NAUTOBOT_NAPALM_USERNAME"
@@ -1,12 +1,19 @@
1
1
  import random
2
2
  import string
3
3
 
4
+ from django.contrib.contenttypes.models import ContentType
4
5
  from django.db.models import Count, Q
5
6
  from django.db.models.fields.related import ManyToManyField
6
7
  from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
7
8
  from django.test import tag
8
9
 
9
- from nautobot.core.filters import RelatedMembershipBooleanFilter, SearchFilter
10
+ from nautobot.core.filters import (
11
+ ContentTypeChoiceFilter,
12
+ ContentTypeFilter,
13
+ ContentTypeMultipleChoiceFilter,
14
+ RelatedMembershipBooleanFilter,
15
+ SearchFilter,
16
+ )
10
17
  from nautobot.core.models.generics import PrimaryModel
11
18
  from nautobot.core.testing import views
12
19
  from nautobot.tenancy import models
@@ -286,6 +293,22 @@ class FilterTestCases:
286
293
  obj, obj_field_name = self._get_nested_related_obj_and_its_field_name(obj, obj_field_name)
287
294
  self._assert_q_filter_predicate_validity(obj, obj_field_name, filter_field_name, lookup_method)
288
295
 
296
+ def test_content_type_related_fields_uses_content_type_filter(self):
297
+ for field in self.queryset.model._meta.fields:
298
+ related_model = getattr(field, "related_model", None)
299
+ if not related_model or related_model != ContentType:
300
+ continue
301
+ with self.subTest(
302
+ f"Assert {self.filterset.__class__.__name__}.{field.name} implements ContentTypeFilter"
303
+ ):
304
+ filter_field = self.filterset.get_filters().get(field.name)
305
+ if not filter_field:
306
+ # This field is not part of the Filterset.
307
+ continue
308
+ self.assertIsInstance(
309
+ filter_field, (ContentTypeFilter, ContentTypeMultipleChoiceFilter, ContentTypeChoiceFilter)
310
+ )
311
+
289
312
  class NameOnlyFilterTestCase(FilterTestCase):
290
313
  """Add simple tests for filtering by name."""
291
314
 
@@ -213,6 +213,8 @@ class ViewTestCases:
213
213
  escape(str(instance.cf.get(custom_field.key) or "")), response_body, msg=response_body
214
214
  )
215
215
 
216
+ return response # for consumption by child test cases if desired
217
+
216
218
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
217
219
  def test_get_object_with_constrained_permission(self):
218
220
  instance1, instance2 = self._get_queryset().all()[:2]
@@ -230,11 +232,14 @@ class ViewTestCases:
230
232
  obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
231
233
 
232
234
  # Try GET to permitted object
233
- self.assertHttpStatus(self.client.get(instance1.get_absolute_url()), 200)
235
+ response = self.client.get(instance1.get_absolute_url())
236
+ self.assertHttpStatus(response, 200)
234
237
 
235
238
  # Try GET to non-permitted object
236
239
  self.assertHttpStatus(self.client.get(instance2.get_absolute_url()), 404)
237
240
 
241
+ return response # for consumption by child test cases if desired
242
+
238
243
  @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
239
244
  def test_has_advanced_tab(self):
240
245
  instance = self._get_queryset().first()
@@ -740,6 +745,13 @@ class ViewTestCases:
740
745
  def get_list_view(self):
741
746
  return lookup.get_view_for_model(self.model, view_type="List")
742
747
 
748
+ def test_list_view_has_filter_form(self):
749
+ view = self.get_list_view()
750
+ if hasattr(view, "filterset_form"): # ObjectListView
751
+ self.assertIsNotNone(view.filterset_form, "List view lacks a FilterForm")
752
+ if hasattr(view, "filterset_form_class"): # ObjectListViewMixin
753
+ self.assertIsNotNone(view.filterset_form_class, "List viewset lacks a FilterForm")
754
+
743
755
  def test_table_with_indentation_is_removed_on_filter_or_sort(self):
744
756
  self.user.is_superuser = True
745
757
  self.user.save()
@@ -215,11 +215,28 @@ def handle_protectederror(obj_list, request, e):
215
215
  protected_count,
216
216
  )
217
217
 
218
+ # Format objects based on whether they have a detail view/absolute url
219
+ objects_with_absolute_url = []
220
+ objects_without_absolute_url = []
218
221
  # Append dependent objects to error message
222
+ for dependent in protected_objects[:50]:
223
+ try:
224
+ dependent.get_absolute_url()
225
+ objects_with_absolute_url.append(dependent)
226
+ except AttributeError:
227
+ objects_without_absolute_url.append(dependent)
228
+
219
229
  err_message += format_html_join(
220
230
  ", ",
221
231
  '<a href="{}">{}</a>',
222
- ((dependent.get_absolute_url(), dependent) for dependent in protected_objects[:50]),
232
+ ((dependent.get_absolute_url(), dependent) for dependent in objects_with_absolute_url),
233
+ )
234
+ if objects_with_absolute_url and objects_without_absolute_url:
235
+ err_message += format_html(", ")
236
+ err_message += format_html_join(
237
+ ", ",
238
+ "<span>{}</span>",
239
+ ((dependent,) for dependent in objects_without_absolute_url),
223
240
  )
224
241
 
225
242
  messages.error(request, err_message)
@@ -102,13 +102,13 @@ __all__ = (
102
102
  "ControllerManagedDeviceGroupFilterSet",
103
103
  "DeviceBayFilterSet",
104
104
  "DeviceBayTemplateFilterSet",
105
+ "DeviceFamilyFilterSet",
105
106
  "DeviceFilterSet",
106
107
  "DeviceRedundancyGroupFilterSet",
107
108
  "DeviceTypeFilterSet",
108
109
  "DeviceTypeToSoftwareImageFileFilterSet",
109
110
  "FrontPortFilterSet",
110
111
  "FrontPortTemplateFilterSet",
111
- "DeviceFamilyFilterSet",
112
112
  "InterfaceConnectionFilterSet",
113
113
  "InterfaceFilterSet",
114
114
  "InterfaceRedundancyGroupFilterSet",
nautobot/dcim/forms.py CHANGED
@@ -728,6 +728,15 @@ class ManufacturerForm(NautobotModelForm):
728
728
  ]
729
729
 
730
730
 
731
+ class ManufacturerFilterForm(NautobotFilterForm):
732
+ model = Manufacturer
733
+ q = forms.CharField(required=False, label="Search")
734
+ device_types = DynamicModelMultipleChoiceField(
735
+ queryset=DeviceType.objects.all(), to_field_name="model", required=False
736
+ )
737
+ platforms = DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), to_field_name="name", required=False)
738
+
739
+
731
740
  #
732
741
  # Device Family
733
742
  #
@@ -745,6 +754,9 @@ class DeviceFamilyForm(NautobotModelForm):
745
754
  class DeviceFamilyFilterForm(NautobotFilterForm):
746
755
  model = DeviceFamily
747
756
  q = forms.CharField(required=False, label="Search")
757
+ device_types = DynamicModelMultipleChoiceField(
758
+ queryset=DeviceType.objects.all(), to_field_name="model", required=False
759
+ )
748
760
  tags = TagFilterField(model)
749
761
 
750
762
 
@@ -1582,6 +1594,13 @@ class PlatformForm(NautobotModelForm):
1582
1594
  }
1583
1595
 
1584
1596
 
1597
+ class PlatformFilterForm(NautobotFilterForm):
1598
+ model = Platform
1599
+ q = forms.CharField(required=False, label="Search")
1600
+ name = forms.CharField(required=False)
1601
+ network_driver = forms.CharField(required=False)
1602
+
1603
+
1585
1604
  #
1586
1605
  # Devices
1587
1606
  #
@@ -2567,14 +2586,14 @@ class InterfaceBulkEditForm(
2567
2586
  queryset=VLAN.objects.all(),
2568
2587
  required=False,
2569
2588
  query_params={
2570
- "location": "null",
2589
+ "locations": "null",
2571
2590
  },
2572
2591
  )
2573
2592
  tagged_vlans = DynamicModelMultipleChoiceField(
2574
2593
  queryset=VLAN.objects.all(),
2575
2594
  required=False,
2576
2595
  query_params={
2577
- "location": "null",
2596
+ "locations": "null",
2578
2597
  },
2579
2598
  )
2580
2599
  vrf = DynamicModelChoiceField(
@@ -2618,8 +2637,8 @@ class InterfaceBulkEditForm(
2618
2637
  # Limit VLAN choices by Location
2619
2638
  if locations.count() == 1:
2620
2639
  location = locations.first()
2621
- self.fields["untagged_vlan"].widget.add_query_param("location", location.pk)
2622
- self.fields["tagged_vlans"].widget.add_query_param("location", location.pk)
2640
+ self.fields["untagged_vlan"].widget.add_query_param("locations", location.pk)
2641
+ self.fields["tagged_vlans"].widget.add_query_param("locations", location.pk)
2623
2642
 
2624
2643
  # Restrict parent/bridge/LAG interface assignment by device (or VC master)
2625
2644
  if device_count == 1:
@@ -45,9 +45,15 @@ __all__ = (
45
45
  class ManufacturerTable(BaseTable):
46
46
  pk = ToggleColumn()
47
47
  name = tables.LinkColumn()
48
- device_type_count = tables.Column(verbose_name="Device Types")
49
- inventory_item_count = tables.Column(verbose_name="Inventory Items")
50
- platform_count = tables.Column(verbose_name="Platforms")
48
+ device_type_count = LinkedCountColumn(
49
+ viewname="dcim:devicetype_list", url_params={"manufacturer": "name"}, verbose_name="Device Types"
50
+ )
51
+ inventory_item_count = LinkedCountColumn(
52
+ viewname="dcim:inventoryitem_list", url_params={"manufacturer": "name"}, verbose_name="Inventory Items"
53
+ )
54
+ platform_count = LinkedCountColumn(
55
+ viewname="dcim:platform_list", url_params={"manufacturer": "name"}, verbose_name="Platforms"
56
+ )
51
57
  actions = ButtonsColumn(Manufacturer)
52
58
 
53
59
  class Meta(BaseTable.Meta):
@@ -71,7 +77,9 @@ class ManufacturerTable(BaseTable):
71
77
  class DeviceFamilyTable(BaseTable):
72
78
  pk = ToggleColumn()
73
79
  name = tables.Column(linkify=True)
74
- device_type_count = tables.Column(verbose_name="Device Types")
80
+ device_type_count = LinkedCountColumn(
81
+ viewname="dcim:devicetype_list", url_params={"device_family": "name"}, verbose_name="Device Types"
82
+ )
75
83
  actions = ButtonsColumn(DeviceFamily)
76
84
  tags = TagColumn(url_name="dcim:devicefamily_list")
77
85
 
@@ -95,6 +103,8 @@ class DeviceFamilyTable(BaseTable):
95
103
  class DeviceTypeTable(BaseTable):
96
104
  pk = ToggleColumn()
97
105
  model = tables.Column(linkify=True, verbose_name="Device Type")
106
+ manufacturer = tables.Column(linkify=True)
107
+ device_family = tables.Column(linkify=True)
98
108
  is_full_depth = BooleanColumn(verbose_name="Full Depth")
99
109
  device_count = LinkedCountColumn(
100
110
  viewname="dcim:device_list",
@@ -109,6 +119,7 @@ class DeviceTypeTable(BaseTable):
109
119
  "pk",
110
120
  "model",
111
121
  "manufacturer",
122
+ "device_family",
112
123
  "part_number",
113
124
  "u_height",
114
125
  "is_full_depth",
@@ -3398,6 +3398,48 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3398
3398
  "download_url": "https://example.com/software_image_file_test_case.bin",
3399
3399
  }
3400
3400
 
3401
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3402
+ def test_correct_handling_for_model_protected_error(self):
3403
+ platform = Platform.objects.first()
3404
+ software_version_status = Status.objects.get_for_model(SoftwareVersion).first()
3405
+ software_image_file_status = Status.objects.get_for_model(SoftwareImageFile).first()
3406
+ software_version = SoftwareVersion.objects.create(
3407
+ platform=platform, version="Test version 1.0.0", status=software_version_status
3408
+ )
3409
+ software_image_file = SoftwareImageFile.objects.create(
3410
+ software_version=software_version,
3411
+ image_file_name="software_image_file_qs_test_1.bin",
3412
+ status=software_image_file_status,
3413
+ )
3414
+ device_type = DeviceType.objects.first()
3415
+ device_role = Role.objects.get_for_model(Device).first()
3416
+ device_status = Status.objects.get_for_model(Device).first()
3417
+ location = Location.objects.filter(location_type__name="Campus").first()
3418
+ Device.objects.create(
3419
+ device_type=device_type,
3420
+ role=device_role,
3421
+ name="Device 1",
3422
+ location=location,
3423
+ status=device_status,
3424
+ software_version=software_version,
3425
+ )
3426
+ device_type_to_software_image_file = DeviceTypeToSoftwareImageFile.objects.create(
3427
+ device_type=device_type, software_image_file=software_image_file
3428
+ )
3429
+
3430
+ self.add_permissions("dcim.delete_softwareimagefile")
3431
+ pk_list = [software_image_file.pk]
3432
+ data = {
3433
+ "pk": pk_list,
3434
+ "confirm": True,
3435
+ "_confirm": True, # Form button
3436
+ }
3437
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
3438
+ self.assertHttpStatus(response, 200)
3439
+ response_body = response.content.decode(response.charset)
3440
+ # Assert protected error message included in the response body
3441
+ self.assertInHTML(f"<span>{device_type_to_software_image_file}</span>", response_body)
3442
+
3401
3443
 
3402
3444
  class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3403
3445
  model = SoftwareVersion
@@ -3436,6 +3478,48 @@ class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3436
3478
  "pre_release": True,
3437
3479
  }
3438
3480
 
3481
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
3482
+ def test_correct_handling_for_model_protected_error(self):
3483
+ platform = Platform.objects.first()
3484
+ software_version_status = Status.objects.get_for_model(SoftwareVersion).first()
3485
+ software_image_file_status = Status.objects.get_for_model(SoftwareImageFile).first()
3486
+ software_version = SoftwareVersion.objects.create(
3487
+ platform=platform, version="Test version 1.0.0", status=software_version_status
3488
+ )
3489
+ software_image_file = SoftwareImageFile.objects.create(
3490
+ software_version=software_version,
3491
+ image_file_name="software_image_file_qs_test_1.bin",
3492
+ status=software_image_file_status,
3493
+ )
3494
+ device_type = DeviceType.objects.first()
3495
+ device_role = Role.objects.get_for_model(Device).first()
3496
+ device_status = Status.objects.get_for_model(Device).first()
3497
+ location = Location.objects.filter(location_type__name="Campus").first()
3498
+ Device.objects.create(
3499
+ device_type=device_type,
3500
+ role=device_role,
3501
+ name="Device 1",
3502
+ location=location,
3503
+ status=device_status,
3504
+ software_version=software_version,
3505
+ )
3506
+ device_type_to_software_image_file = DeviceTypeToSoftwareImageFile.objects.create(
3507
+ device_type=device_type, software_image_file=software_image_file
3508
+ )
3509
+
3510
+ self.add_permissions("dcim.delete_softwareversion")
3511
+ pk_list = [software_version.pk]
3512
+ data = {
3513
+ "pk": pk_list,
3514
+ "confirm": True,
3515
+ "_confirm": True, # Form button
3516
+ }
3517
+ response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
3518
+ self.assertHttpStatus(response, 200)
3519
+ response_body = response.content.decode(response.charset)
3520
+ # Assert protected error message included in the response body
3521
+ self.assertInHTML(f"<span>{device_type_to_software_image_file}</span>", response_body)
3522
+
3439
3523
 
3440
3524
  class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3441
3525
  model = Controller
nautobot/dcim/views.py CHANGED
@@ -710,6 +710,7 @@ class ManufacturerListView(generic.ObjectListView):
710
710
  platform_count=count_related(Platform, "manufacturer"),
711
711
  )
712
712
  filterset = filters.ManufacturerFilterSet
713
+ filterset_form = forms.ManufacturerFilterForm
713
714
  table = tables.ManufacturerTable
714
715
 
715
716
 
@@ -1212,6 +1213,7 @@ class PlatformListView(generic.ObjectListView):
1212
1213
  virtual_machine_count=count_related(VirtualMachine, "platform"),
1213
1214
  )
1214
1215
  filterset = filters.PlatformFilterSet
1216
+ filterset_form = forms.PlatformFilterForm
1215
1217
  table = tables.PlatformTable
1216
1218
 
1217
1219
 
@@ -3103,6 +3105,7 @@ class InterfaceRedundancyGroupAssociationUIViewSet(ObjectEditViewMixin, ObjectDe
3103
3105
 
3104
3106
  class DeviceFamilyUIViewSet(NautobotUIViewSet):
3105
3107
  filterset_class = filters.DeviceFamilyFilterSet
3108
+ filterset_form_class = forms.DeviceFamilyFilterForm
3106
3109
  form_class = forms.DeviceFamilyForm
3107
3110
  bulk_update_form_class = forms.DeviceFamilyBulkEditForm
3108
3111
  queryset = DeviceFamily.objects.annotate(device_type_count=count_related(DeviceType, "device_family"))
@@ -304,13 +304,13 @@ class DynamicGroupViewSet(NotesViewSetMixin, ModelViewSet):
304
304
  # @extend_schema(methods=["get"], responses={200: member_response})
305
305
  @action(detail=True, methods=["get"])
306
306
  def members(self, request, pk, *args, **kwargs):
307
- """List member objects of the same type as the `content_type` for this dynamic group."""
307
+ """List the member objects of this dynamic group."""
308
308
  instance = get_object_or_404(self.queryset, pk=pk)
309
309
 
310
310
  # Retrieve the serializer for the content_type and paginate the results
311
311
  member_model_class = instance.content_type.model_class()
312
312
  member_serializer_class = get_serializer_for_model(member_model_class)
313
- members = self.paginate_queryset(instance.members)
313
+ members = self.paginate_queryset(instance.members.restrict(request.user, "view"))
314
314
  member_serializer = member_serializer_class(members, many=True, context={"request": request})
315
315
  return self.get_paginated_response(member_serializer.data)
316
316
 
@@ -91,9 +91,12 @@ class ChangeContext:
91
91
  for entry in self.deferred_object_changes[key]:
92
92
  objectchange = entry["instance"].to_objectchange(entry["action"])
93
93
  objectchange.user = entry["user"]
94
+ objectchange.user_name = objectchange.user.username
94
95
  objectchange.request_id = self.change_id
95
96
  objectchange.change_context = self.context
96
97
  objectchange.change_context_detail = self.context_detail[:CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL]
98
+ if not objectchange.changed_object_id:
99
+ objectchange.changed_object_id = entry.get("changed_object_id")
97
100
  create_object_changes.append(objectchange)
98
101
  self.deferred_object_changes.pop(key, None)
99
102
  ObjectChange.objects.bulk_create(create_object_changes, batch_size=batch_size)
@@ -504,7 +504,7 @@ class ContactFilterSet(ContactTeamFilterSet):
504
504
  fields = "__all__"
505
505
 
506
506
 
507
- class ContactAssociationFilterSet(NautobotFilterSet):
507
+ class ContactAssociationFilterSet(NautobotFilterSet, StatusModelFilterSetMixin, RoleModelFilterSetMixin):
508
508
  q = SearchFilter(
509
509
  filter_predicates={
510
510
  "contact__name": "icontains",
@@ -512,6 +512,19 @@ class ContactAssociationFilterSet(NautobotFilterSet):
512
512
  },
513
513
  )
514
514
 
515
+ contact = NaturalKeyOrPKMultipleChoiceFilter(
516
+ queryset=Contact.objects.all(),
517
+ to_field_name="name",
518
+ label="Contact (name or ID)",
519
+ )
520
+ team = NaturalKeyOrPKMultipleChoiceFilter(
521
+ queryset=Team.objects.all(),
522
+ to_field_name="name",
523
+ label="Team (name or ID)",
524
+ )
525
+
526
+ associated_object_type = ContentTypeFilter()
527
+
515
528
  class Meta:
516
529
  model = ContactAssociation
517
530
  fields = "__all__"
@@ -610,6 +623,7 @@ class ExportTemplateFilterSet(BaseFilterSet):
610
623
  },
611
624
  )
612
625
  owner_content_type = ContentTypeFilter()
626
+ content_type = ContentTypeFilter()
613
627
 
614
628
  class Meta:
615
629
  model = ExportTemplate
@@ -104,6 +104,7 @@ __all__ = (
104
104
  "ConfigContextSchemaBulkEditForm",
105
105
  "ConfigContextSchemaFilterForm",
106
106
  "CustomFieldForm",
107
+ "CustomFieldFilterForm",
107
108
  "CustomFieldModelCSVForm",
108
109
  "CustomFieldBulkCreateForm", # 2.0 TODO remove this deprecated class
109
110
  "CustomFieldChoiceFormSet",
@@ -114,6 +115,7 @@ __all__ = (
114
115
  "DynamicGroupMembershipFormSet",
115
116
  "ExportTemplateForm",
116
117
  "ExportTemplateFilterForm",
118
+ "ExternalIntegrationFilterForm",
117
119
  "ExternalIntegrationForm",
118
120
  "ExternalIntegrationBulkEditForm",
119
121
  "GitRepositoryForm",
@@ -144,6 +146,7 @@ __all__ = (
144
146
  "RelationshipFilterForm",
145
147
  "RelationshipAssociationFilterForm",
146
148
  "RoleBulkEditForm",
149
+ "RoleFilterForm",
147
150
  "RoleForm",
148
151
  "ScheduledJobFilterForm",
149
152
  "SecretForm",
@@ -421,6 +424,17 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
421
424
  self.fields["key"].widget.attrs["readonly"] = True
422
425
 
423
426
 
427
+ class CustomFieldFilterForm(NautobotFilterForm):
428
+ model = CustomField
429
+ q = forms.CharField(required=False, label="Search")
430
+ content_types = MultipleContentTypeField(
431
+ queryset=ContentType.objects.filter(FeatureQuery("custom_fields").get_query()),
432
+ choices_as_strings=True,
433
+ required=False,
434
+ label="Content Type(s)",
435
+ )
436
+
437
+
424
438
  class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelFormMixin):
425
439
  """
426
440
  Base class for CSV/JSON/YAML import of models that support custom fields.
@@ -638,6 +652,14 @@ class ExternalIntegrationBulkEditForm(NautobotBulkEditForm):
638
652
  nullable_fields = ["extra_config", "secrets_group", "headers"]
639
653
 
640
654
 
655
+ class ExternalIntegrationFilterForm(NautobotFilterForm):
656
+ model = ExternalIntegration
657
+ q = forms.CharField(required=False, label="Search")
658
+ secrets_group = DynamicModelMultipleChoiceField(
659
+ queryset=SecretsGroup.objects.all(), to_field_name="name", required=False
660
+ )
661
+
662
+
641
663
  #
642
664
  # Git repositories and other data sources
643
665
  #
@@ -1429,6 +1451,17 @@ class RoleBulkEditForm(NautobotBulkEditForm):
1429
1451
  nullable_fields = ["weight"]
1430
1452
 
1431
1453
 
1454
+ class RoleFilterForm(NautobotFilterForm):
1455
+ model = Role
1456
+ q = forms.CharField(required=False, label="Search")
1457
+ content_types = MultipleContentTypeField(
1458
+ queryset=RoleModelsQuery().as_queryset(),
1459
+ required=False,
1460
+ choices_as_strings=True,
1461
+ label="Content Type(s)",
1462
+ )
1463
+
1464
+
1432
1465
  #
1433
1466
  # Secrets
1434
1467
  #
@@ -45,7 +45,6 @@ __all__ = (
45
45
  # 2.0 TODO: remove the below deprecated aliases
46
46
  "AddRemoveTagsForm",
47
47
  "CustomFieldBulkEditForm",
48
- "CustomFieldFilterForm",
49
48
  "CustomFieldModelForm",
50
49
  "RelationshipModelForm",
51
50
  "RoleModelBulkEditFormMixin",
@@ -767,11 +766,6 @@ class CustomFieldBulkEditForm(CustomFieldModelBulkEditFormMixin):
767
766
  pass
768
767
 
769
768
 
770
- @class_deprecated_in_favor_of(CustomFieldModelFilterFormMixin)
771
- class CustomFieldFilterForm(CustomFieldModelFilterFormMixin):
772
- pass
773
-
774
-
775
769
  @class_deprecated_in_favor_of(CustomFieldModelFormMixin)
776
770
  class CustomFieldModelForm(CustomFieldModelFormMixin):
777
771
  pass
@@ -246,7 +246,12 @@ def _handle_deleted_object(sender, instance, **kwargs):
246
246
 
247
247
  if save_new_objectchange:
248
248
  change_context.deferred_object_changes.setdefault(unique_object_change_id, []).append(
249
- {"action": ObjectChangeActionChoices.ACTION_DELETE, "instance": instance, "user": user}
249
+ {
250
+ "action": ObjectChangeActionChoices.ACTION_DELETE,
251
+ "instance": instance,
252
+ "user": user,
253
+ "changed_object_id": instance.pk,
254
+ }
250
255
  )
251
256
  if not change_context.defer_object_changes:
252
257
  objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)