nautobot 2.2.3__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 (327) 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.py +6 -4
  6. nautobot/core/settings.yaml +54 -19
  7. nautobot/core/templates/admin/base.html +2 -2
  8. nautobot/core/templates/base_django.html +2 -2
  9. nautobot/core/templates/buttons/export.html +47 -47
  10. nautobot/core/templates/inc/javascript.html +3 -0
  11. nautobot/core/templates/inc/media.html +3 -0
  12. nautobot/core/templates/login.html +2 -2
  13. nautobot/core/templates/nautobot_config.py.j2 +2 -0
  14. nautobot/core/testing/filters.py +24 -1
  15. nautobot/core/testing/views.py +13 -1
  16. nautobot/core/tests/test_jobs.py +79 -2
  17. nautobot/core/tests/test_views.py +33 -0
  18. nautobot/core/views/mixins.py +4 -0
  19. nautobot/core/views/utils.py +18 -1
  20. nautobot/dcim/filters/__init__.py +1 -1
  21. nautobot/dcim/forms.py +23 -4
  22. nautobot/dcim/tables/devicetypes.py +15 -4
  23. nautobot/dcim/tests/test_views.py +323 -55
  24. nautobot/dcim/views.py +26 -20
  25. nautobot/extras/api/serializers.py +17 -6
  26. nautobot/extras/api/views.py +2 -2
  27. nautobot/extras/context_managers.py +3 -0
  28. nautobot/extras/filters/__init__.py +15 -1
  29. nautobot/extras/forms/forms.py +33 -0
  30. nautobot/extras/forms/mixins.py +0 -6
  31. nautobot/extras/signals.py +6 -1
  32. nautobot/extras/tests/test_api.py +24 -2
  33. nautobot/extras/tests/test_context_managers.py +51 -1
  34. nautobot/extras/tests/test_filters.py +69 -0
  35. nautobot/extras/tests/test_forms.py +0 -3
  36. nautobot/extras/tests/test_views.py +48 -4
  37. nautobot/extras/utils.py +2 -1
  38. nautobot/extras/views.py +47 -31
  39. nautobot/ipam/forms.py +18 -0
  40. nautobot/ipam/tests/test_views.py +9 -2
  41. nautobot/ipam/views.py +17 -6
  42. nautobot/project-static/docs/404.html +107 -51
  43. nautobot/project-static/docs/apps/index.html +107 -51
  44. nautobot/project-static/docs/apps/nautobot-apps.html +107 -51
  45. nautobot/project-static/docs/assets/_mkdocstrings.css +6 -1
  46. nautobot/project-static/docs/assets/extra.css +7 -0
  47. nautobot/project-static/docs/assets/javascripts/bundle.ebd0bdb7.min.js +29 -0
  48. nautobot/project-static/docs/assets/javascripts/bundle.ebd0bdb7.min.js.map +7 -0
  49. nautobot/project-static/docs/assets/stylesheets/main.6543a935.min.css +1 -0
  50. nautobot/project-static/docs/assets/stylesheets/main.6543a935.min.css.map +1 -0
  51. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +107 -51
  52. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +107 -51
  53. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +107 -51
  54. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +107 -51
  55. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +107 -51
  56. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +107 -51
  57. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +107 -51
  58. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +107 -51
  59. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +107 -51
  60. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +107 -51
  61. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +107 -51
  62. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +107 -51
  63. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +107 -51
  64. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +107 -51
  65. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +107 -51
  66. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +107 -51
  67. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +107 -51
  68. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +107 -51
  69. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +107 -51
  70. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +107 -51
  71. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +107 -51
  72. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +107 -51
  73. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +107 -51
  74. nautobot/project-static/docs/development/apps/api/configuration-view.html +110 -54
  75. nautobot/project-static/docs/development/apps/api/database-backend-config.html +110 -54
  76. nautobot/project-static/docs/development/apps/api/models/django-admin.html +107 -51
  77. nautobot/project-static/docs/development/apps/api/models/global-search.html +110 -54
  78. nautobot/project-static/docs/development/apps/api/models/graphql.html +113 -57
  79. nautobot/project-static/docs/development/apps/api/models/index.html +107 -51
  80. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +113 -57
  81. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +107 -51
  82. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +111 -55
  83. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +107 -51
  84. nautobot/project-static/docs/development/apps/api/platform-features/index.html +107 -51
  85. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +110 -54
  86. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +110 -54
  87. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +110 -54
  88. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +110 -54
  89. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +107 -51
  90. nautobot/project-static/docs/development/apps/api/prometheus.html +110 -54
  91. nautobot/project-static/docs/development/apps/api/setup.html +107 -51
  92. nautobot/project-static/docs/development/apps/api/testing.html +113 -57
  93. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +110 -54
  94. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +110 -54
  95. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +107 -51
  96. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +107 -51
  97. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +113 -57
  98. nautobot/project-static/docs/development/apps/api/views/base-template.html +107 -51
  99. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +110 -54
  100. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +107 -51
  101. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +110 -54
  102. nautobot/project-static/docs/development/apps/api/views/index.html +107 -51
  103. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +113 -57
  104. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +122 -66
  105. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +110 -54
  106. nautobot/project-static/docs/development/apps/api/views/notes.html +110 -54
  107. nautobot/project-static/docs/development/apps/api/views/rest-api.html +107 -51
  108. nautobot/project-static/docs/development/apps/api/views/urls.html +107 -51
  109. nautobot/project-static/docs/development/apps/index.html +128 -72
  110. nautobot/project-static/docs/development/apps/migration/code-updates.html +107 -51
  111. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +107 -51
  112. nautobot/project-static/docs/development/apps/migration/from-v1.html +109 -53
  113. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +107 -51
  114. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +107 -51
  115. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +107 -51
  116. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +107 -51
  117. nautobot/project-static/docs/development/apps/porting-from-netbox.html +110 -54
  118. nautobot/project-static/docs/development/core/application-registry.html +120 -64
  119. nautobot/project-static/docs/development/core/best-practices.html +122 -66
  120. nautobot/project-static/docs/development/core/bootstrap-ui.html +107 -51
  121. nautobot/project-static/docs/development/core/caching.html +107 -51
  122. nautobot/project-static/docs/development/core/controllers.html +107 -51
  123. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +113 -57
  124. nautobot/project-static/docs/development/core/generic-views.html +110 -54
  125. nautobot/project-static/docs/development/core/getting-started.html +137 -81
  126. nautobot/project-static/docs/development/core/homepage.html +110 -54
  127. nautobot/project-static/docs/development/core/index.html +107 -51
  128. nautobot/project-static/docs/development/core/model-checklist.html +107 -51
  129. nautobot/project-static/docs/development/core/model-features.html +107 -51
  130. nautobot/project-static/docs/development/core/natural-keys.html +110 -54
  131. nautobot/project-static/docs/development/core/navigation-menu.html +107 -51
  132. nautobot/project-static/docs/development/core/release-checklist.html +107 -51
  133. nautobot/project-static/docs/development/core/role-internals.html +107 -51
  134. nautobot/project-static/docs/development/core/settings.html +107 -51
  135. nautobot/project-static/docs/development/core/style-guide.html +110 -54
  136. nautobot/project-static/docs/development/core/templates.html +113 -57
  137. nautobot/project-static/docs/development/core/testing.html +126 -70
  138. nautobot/project-static/docs/development/core/user-preferences.html +107 -51
  139. nautobot/project-static/docs/development/index.html +107 -51
  140. nautobot/project-static/docs/development/jobs/index.html +173 -117
  141. nautobot/project-static/docs/development/jobs/migration/from-v1.html +110 -54
  142. nautobot/project-static/docs/docker/index.html +3 -3
  143. nautobot/project-static/docs/index.html +125 -69
  144. nautobot/project-static/docs/installation/selinux-troubleshooting.html +3 -3
  145. nautobot/project-static/docs/release-notes/index.html +107 -51
  146. nautobot/project-static/docs/release-notes/version-1.0.html +108 -52
  147. nautobot/project-static/docs/release-notes/version-1.1.html +107 -51
  148. nautobot/project-static/docs/release-notes/version-1.2.html +109 -53
  149. nautobot/project-static/docs/release-notes/version-1.3.html +108 -52
  150. nautobot/project-static/docs/release-notes/version-1.4.html +109 -53
  151. nautobot/project-static/docs/release-notes/version-1.5.html +118 -62
  152. nautobot/project-static/docs/release-notes/version-1.6.html +721 -285
  153. nautobot/project-static/docs/release-notes/version-2.0.html +113 -57
  154. nautobot/project-static/docs/release-notes/version-2.1.html +107 -51
  155. nautobot/project-static/docs/release-notes/version-2.2.html +503 -120
  156. nautobot/project-static/docs/requirements.txt +4 -4
  157. nautobot/project-static/docs/search/search_index.json +1 -1
  158. nautobot/project-static/docs/sitemap.xml +262 -262
  159. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  160. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +107 -51
  161. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +107 -51
  162. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +109 -53
  163. nautobot/project-static/docs/user-guide/administration/configuration/index.html +108 -52
  164. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +254 -167
  165. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +113 -57
  166. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +107 -51
  167. nautobot/project-static/docs/user-guide/administration/guides/caching.html +113 -57
  168. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +107 -51
  169. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +107 -51
  170. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +107 -51
  171. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +113 -57
  172. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +108 -52
  173. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +107 -51
  174. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +107 -51
  175. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +171 -112
  176. nautobot/project-static/docs/user-guide/administration/installation/docker.html +13 -8626
  177. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +117 -61
  178. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +13 -8614
  179. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +252 -165
  180. nautobot/project-static/docs/user-guide/administration/installation/index.html +165 -192
  181. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +411 -691
  182. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +249 -230
  183. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +13 -8118
  184. nautobot/project-static/docs/user-guide/administration/installation/services.html +351 -241
  185. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +8684 -0
  186. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +8672 -0
  187. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +8176 -0
  188. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +110 -54
  189. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +110 -54
  190. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +155 -99
  191. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +107 -51
  192. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +109 -53
  193. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +107 -51
  194. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +107 -51
  195. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +108 -52
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +107 -51
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +107 -51
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +107 -70
  199. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +115 -59
  200. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +113 -57
  201. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +107 -51
  202. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +107 -51
  203. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +107 -51
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +107 -51
  205. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +110 -54
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +107 -51
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +110 -54
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +110 -54
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +110 -54
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +110 -54
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +107 -51
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +107 -51
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +115 -59
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +110 -54
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +110 -54
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +110 -54
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +110 -54
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +117 -61
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +110 -54
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +110 -54
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +119 -63
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +110 -54
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +110 -54
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +113 -57
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +113 -57
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +113 -57
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +107 -51
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +113 -57
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +107 -51
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +110 -54
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +110 -54
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +107 -51
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +110 -54
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +110 -54
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +107 -51
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +107 -51
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +107 -51
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +110 -54
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +110 -54
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +110 -54
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +110 -54
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +107 -51
  243. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +113 -57
  244. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +110 -54
  245. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +110 -54
  246. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +110 -54
  247. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +125 -69
  248. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +113 -57
  249. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +128 -72
  250. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +110 -54
  251. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +107 -51
  252. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +110 -54
  253. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +116 -60
  254. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +107 -51
  255. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +113 -57
  256. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +107 -51
  257. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +110 -54
  258. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +107 -51
  259. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +107 -51
  260. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +107 -51
  261. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +107 -51
  262. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +113 -57
  263. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +113 -57
  264. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +107 -51
  265. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +113 -57
  266. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +110 -54
  267. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +107 -51
  268. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +107 -51
  269. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +108 -52
  270. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +107 -51
  271. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +107 -51
  272. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +107 -51
  273. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +107 -51
  274. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +108 -52
  275. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +110 -54
  276. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +113 -57
  277. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +107 -51
  278. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +110 -54
  279. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +107 -51
  280. nautobot/project-static/docs/user-guide/index.html +109 -53
  281. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +107 -51
  282. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +113 -57
  283. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +128 -72
  284. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +107 -51
  285. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +125 -69
  286. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +107 -51
  287. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +110 -54
  288. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +125 -69
  289. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +110 -54
  290. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +107 -51
  291. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +107 -51
  292. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +128 -72
  293. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +114 -58
  294. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +113 -57
  295. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +110 -54
  296. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +121 -65
  297. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +109 -53
  298. nautobot/project-static/docs/user-guide/platform-functionality/note.html +110 -54
  299. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +116 -60
  300. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +110 -54
  301. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +131 -75
  302. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +149 -93
  303. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +110 -54
  304. nautobot/project-static/docs/user-guide/platform-functionality/role.html +108 -84
  305. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +116 -60
  306. nautobot/project-static/docs/user-guide/platform-functionality/status.html +119 -63
  307. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +110 -54
  308. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +137 -81
  309. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +110 -54
  310. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +107 -51
  311. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +110 -54
  312. nautobot/project-static/js/forms.js +2 -1
  313. nautobot/tenancy/forms.py +9 -0
  314. nautobot/tenancy/views.py +3 -6
  315. nautobot/virtualization/forms.py +18 -6
  316. nautobot/virtualization/templates/virtualization/clustertype.html +2 -2
  317. nautobot/virtualization/views.py +7 -9
  318. {nautobot-2.2.3.dist-info → nautobot-2.2.5.dist-info}/METADATA +2 -2
  319. {nautobot-2.2.3.dist-info → nautobot-2.2.5.dist-info}/RECORD +323 -320
  320. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js +0 -29
  321. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js.map +0 -7
  322. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +0 -1
  323. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +0 -1
  324. {nautobot-2.2.3.dist-info → nautobot-2.2.5.dist-info}/LICENSE.txt +0 -0
  325. {nautobot-2.2.3.dist-info → nautobot-2.2.5.dist-info}/NOTICE +0 -0
  326. {nautobot-2.2.3.dist-info → nautobot-2.2.5.dist-info}/WHEEL +0 -0
  327. {nautobot-2.2.3.dist-info → nautobot-2.2.5.dist-info}/entry_points.txt +0 -0
nautobot/dcim/views.py CHANGED
@@ -199,6 +199,7 @@ class LocationTypeView(generic.ObjectView):
199
199
  return {
200
200
  "children_table": children_table,
201
201
  "locations_table": locations_table,
202
+ **super().get_extra_context(request, instance),
202
203
  }
203
204
 
204
205
 
@@ -297,6 +298,7 @@ class LocationView(generic.ObjectView):
297
298
  "contact_association_permission": ["extras.add_contactassociation"],
298
299
  # show the button if any of these fields have non-empty value.
299
300
  "show_convert_to_contact_button": instance.contact_name or instance.contact_phone or instance.contact_email,
301
+ **super().get_extra_context(request, instance),
300
302
  }
301
303
 
302
304
 
@@ -494,9 +496,7 @@ class RackGroupView(generic.ObjectView):
494
496
  }
495
497
  RequestConfig(request, paginate).configure(rack_table)
496
498
 
497
- return {
498
- "rack_table": rack_table,
499
- }
499
+ return {"rack_table": rack_table, **super().get_extra_context(request, instance)}
500
500
 
501
501
 
502
502
  class RackGroupEditView(generic.ObjectEditView):
@@ -615,6 +615,7 @@ class RackView(generic.ObjectView):
615
615
  "nonracked_devices": nonracked_devices,
616
616
  "next_rack": next_rack,
617
617
  "prev_rack": prev_rack,
618
+ **super().get_extra_context(request, instance),
618
619
  }
619
620
 
620
621
 
@@ -709,6 +710,7 @@ class ManufacturerListView(generic.ObjectListView):
709
710
  platform_count=count_related(Platform, "manufacturer"),
710
711
  )
711
712
  filterset = filters.ManufacturerFilterSet
713
+ filterset_form = forms.ManufacturerFilterForm
712
714
  table = tables.ManufacturerTable
713
715
 
714
716
 
@@ -731,9 +733,7 @@ class ManufacturerView(generic.ObjectView):
731
733
  }
732
734
  RequestConfig(request, paginate).configure(device_table)
733
735
 
734
- return {
735
- "device_table": device_table,
736
- }
736
+ return {"device_table": device_table, **super().get_extra_context(request, instance)}
737
737
 
738
738
 
739
739
  class ManufacturerEditView(generic.ObjectEditView):
@@ -839,6 +839,7 @@ class DeviceTypeView(generic.ObjectView):
839
839
  "rear_port_table": rear_port_table,
840
840
  "devicebay_table": devicebay_table,
841
841
  "software_image_files_table": software_image_files_table,
842
+ **super().get_extra_context(request, instance),
842
843
  }
843
844
 
844
845
 
@@ -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
 
@@ -1237,6 +1239,7 @@ class PlatformView(generic.ObjectView):
1237
1239
  return {
1238
1240
  "device_table": device_table,
1239
1241
  "network_driver_tool_names": get_network_driver_mapping_tool_names(),
1242
+ **super().get_extra_context(request, instance),
1240
1243
  }
1241
1244
 
1242
1245
 
@@ -1246,7 +1249,10 @@ class PlatformEditView(generic.ObjectEditView):
1246
1249
  template_name = "dcim/platform_edit.html"
1247
1250
 
1248
1251
  def get_extra_context(self, request, instance):
1249
- return {"network_driver_names": sorted(get_all_network_driver_mappings().keys())}
1252
+ return {
1253
+ "network_driver_names": sorted(get_all_network_driver_mappings().keys()),
1254
+ **super().get_extra_context(request, instance),
1255
+ }
1250
1256
 
1251
1257
 
1252
1258
  class PlatformDeleteView(generic.ObjectDeleteView):
@@ -1637,7 +1643,7 @@ class ConsolePortView(generic.ObjectView):
1637
1643
  queryset = ConsolePort.objects.all()
1638
1644
 
1639
1645
  def get_extra_context(self, request, instance):
1640
- return {"breadcrumb_url": "dcim:device_consoleports"}
1646
+ return {"breadcrumb_url": "dcim:device_consoleports", **super().get_extra_context(request, instance)}
1641
1647
 
1642
1648
 
1643
1649
  class ConsolePortCreateView(generic.ComponentCreateView):
@@ -1699,7 +1705,7 @@ class ConsoleServerPortView(generic.ObjectView):
1699
1705
  queryset = ConsoleServerPort.objects.all()
1700
1706
 
1701
1707
  def get_extra_context(self, request, instance):
1702
- return {"breadcrumb_url": "dcim:device_consoleserverports"}
1708
+ return {"breadcrumb_url": "dcim:device_consoleserverports", **super().get_extra_context(request, instance)}
1703
1709
 
1704
1710
 
1705
1711
  class ConsoleServerPortCreateView(generic.ComponentCreateView):
@@ -1761,7 +1767,7 @@ class PowerPortView(generic.ObjectView):
1761
1767
  queryset = PowerPort.objects.all()
1762
1768
 
1763
1769
  def get_extra_context(self, request, instance):
1764
- return {"breadcrumb_url": "dcim:device_powerports"}
1770
+ return {"breadcrumb_url": "dcim:device_powerports", **super().get_extra_context(request, instance)}
1765
1771
 
1766
1772
 
1767
1773
  class PowerPortCreateView(generic.ComponentCreateView):
@@ -1823,7 +1829,7 @@ class PowerOutletView(generic.ObjectView):
1823
1829
  queryset = PowerOutlet.objects.all()
1824
1830
 
1825
1831
  def get_extra_context(self, request, instance):
1826
- return {"breadcrumb_url": "dcim:device_poweroutlets"}
1832
+ return {"breadcrumb_url": "dcim:device_poweroutlets", **super().get_extra_context(request, instance)}
1827
1833
 
1828
1834
 
1829
1835
  class PowerOutletCreateView(generic.ComponentCreateView):
@@ -1919,6 +1925,7 @@ class InterfaceView(generic.ObjectView):
1919
1925
  "breadcrumb_url": "dcim:device_interfaces",
1920
1926
  "child_interfaces_table": child_interfaces_tables,
1921
1927
  "redundancy_table": redundancy_table,
1928
+ **super().get_extra_context(request, instance),
1922
1929
  }
1923
1930
 
1924
1931
  def _get_interface_redundancy_groups_table(self, request, instance):
@@ -2005,7 +2012,7 @@ class FrontPortView(generic.ObjectView):
2005
2012
  queryset = FrontPort.objects.all()
2006
2013
 
2007
2014
  def get_extra_context(self, request, instance):
2008
- return {"breadcrumb_url": "dcim:device_frontports"}
2015
+ return {"breadcrumb_url": "dcim:device_frontports", **super().get_extra_context(request, instance)}
2009
2016
 
2010
2017
 
2011
2018
  class FrontPortCreateView(generic.ComponentCreateView):
@@ -2067,7 +2074,7 @@ class RearPortView(generic.ObjectView):
2067
2074
  queryset = RearPort.objects.all()
2068
2075
 
2069
2076
  def get_extra_context(self, request, instance):
2070
- return {"breadcrumb_url": "dcim:device_rearports"}
2077
+ return {"breadcrumb_url": "dcim:device_rearports", **super().get_extra_context(request, instance)}
2071
2078
 
2072
2079
 
2073
2080
  class RearPortCreateView(generic.ComponentCreateView):
@@ -2129,7 +2136,7 @@ class DeviceBayView(generic.ObjectView):
2129
2136
  queryset = DeviceBay.objects.all()
2130
2137
 
2131
2138
  def get_extra_context(self, request, instance):
2132
- return {"breadcrumb_url": "dcim:device_devicebays"}
2139
+ return {"breadcrumb_url": "dcim:device_devicebays", **super().get_extra_context(request, instance)}
2133
2140
 
2134
2141
 
2135
2142
  class DeviceBayCreateView(generic.ComponentCreateView):
@@ -2281,6 +2288,7 @@ class InventoryItemView(generic.ObjectView):
2281
2288
  return {
2282
2289
  "breadcrumb_url": "dcim:device_inventory",
2283
2290
  "software_version_images": software_version_images,
2291
+ **super().get_extra_context(request, instance),
2284
2292
  }
2285
2293
 
2286
2294
 
@@ -2488,6 +2496,7 @@ class PathTraceView(generic.ObjectView):
2488
2496
  "path": path,
2489
2497
  "related_paths": related_paths,
2490
2498
  "total_length": path.get_total_length() if path else None,
2499
+ **super().get_extra_context(request, instance),
2491
2500
  }
2492
2501
 
2493
2502
 
@@ -2698,9 +2707,7 @@ class VirtualChassisView(generic.ObjectView):
2698
2707
  def get_extra_context(self, request, instance):
2699
2708
  members = Device.objects.restrict(request.user).filter(virtual_chassis=instance)
2700
2709
 
2701
- return {
2702
- "members": members,
2703
- }
2710
+ return {"members": members, **super().get_extra_context(request, instance)}
2704
2711
 
2705
2712
 
2706
2713
  class VirtualChassisCreateView(generic.ObjectEditView):
@@ -2938,9 +2945,7 @@ class PowerPanelView(generic.ObjectView):
2938
2945
  powerfeed_table = tables.PowerFeedTable(data=power_feeds, orderable=False)
2939
2946
  powerfeed_table.exclude = ["power_panel"]
2940
2947
 
2941
- return {
2942
- "powerfeed_table": powerfeed_table,
2943
- }
2948
+ return {"powerfeed_table": powerfeed_table, **super().get_extra_context(request, instance)}
2944
2949
 
2945
2950
 
2946
2951
  class PowerPanelEditView(generic.ObjectEditView):
@@ -3100,6 +3105,7 @@ class InterfaceRedundancyGroupAssociationUIViewSet(ObjectEditViewMixin, ObjectDe
3100
3105
 
3101
3106
  class DeviceFamilyUIViewSet(NautobotUIViewSet):
3102
3107
  filterset_class = filters.DeviceFamilyFilterSet
3108
+ filterset_form_class = forms.DeviceFamilyFilterForm
3103
3109
  form_class = forms.DeviceFamilyForm
3104
3110
  bulk_update_form_class = forms.DeviceFamilyBulkEditForm
3105
3111
  queryset = DeviceFamily.objects.annotate(device_type_count=count_related(DeviceType, "device_family"))
@@ -223,17 +223,28 @@ class ContactAssociationSerializer(NautobotModelSerializer):
223
223
  }
224
224
 
225
225
  def validate(self, data):
226
- # Validate uniqueness of (contact/team, role)
226
+ # Validate uniqueness of (associated object, associated object type, contact/team, role)
227
+ unique_together_fields = None
228
+
227
229
  if data.get("contact") and data.get("role"):
228
- validator = UniqueTogetherValidator(
229
- queryset=ContactAssociation.objects.all(),
230
- fields=("contact", "role"),
230
+ unique_together_fields = (
231
+ "associated_object_type",
232
+ "associated_object_id",
233
+ "contact",
234
+ "role",
231
235
  )
232
- validator(data, self)
233
236
  elif data.get("team") and data.get("role"):
237
+ unique_together_fields = (
238
+ "associated_object_type",
239
+ "associated_object_id",
240
+ "team",
241
+ "role",
242
+ )
243
+
244
+ if unique_together_fields is not None:
234
245
  validator = UniqueTogetherValidator(
235
246
  queryset=ContactAssociation.objects.all(),
236
- fields=("team", "role"),
247
+ fields=unique_together_fields,
237
248
  )
238
249
  validator(data, self)
239
250
 
@@ -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)
@@ -17,6 +17,7 @@ from nautobot.core.models.fields import slugify_dashes_to_underscores
17
17
  from nautobot.core.testing import APITestCase, APIViewTestCases
18
18
  from nautobot.core.testing.utils import disable_warnings
19
19
  from nautobot.core.utils.lookup import get_route_for_model
20
+ from nautobot.core.utils.permissions import get_permission_for_model
20
21
  from nautobot.dcim.models import (
21
22
  Controller,
22
23
  Device,
@@ -768,7 +769,7 @@ class DynamicGroupTestMixin:
768
769
 
769
770
  # Then the DynamicGroups.
770
771
  cls.content_type = ContentType.objects.get_for_model(Device)
771
- cls.groups = cls.groups = [
772
+ cls.groups = [
772
773
  DynamicGroup.objects.create(
773
774
  name="API DynamicGroup 1",
774
775
  content_type=cls.content_type,
@@ -811,13 +812,34 @@ class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
811
812
  def test_get_members(self):
812
813
  """Test that the `/members/` API endpoint returns what is expected."""
813
814
  self.add_permissions("extras.view_dynamicgroup")
814
- instance = DynamicGroup.objects.first()
815
+ instance = self.groups[0]
816
+ self.add_permissions(get_permission_for_model(instance.content_type.model_class(), "view"))
815
817
  member_count = instance.members.count()
816
818
  url = reverse("extras-api:dynamicgroup-members", kwargs={"pk": instance.pk})
817
819
  response = self.client.get(url, **self.header)
818
820
  self.assertHttpStatus(response, status.HTTP_200_OK)
819
821
  self.assertEqual(member_count, len(response.json()["results"]))
820
822
 
823
+ def test_get_members_with_constrained_permission(self):
824
+ """Test that the `/members/` API endpoint enforces permissions on the member model."""
825
+ self.add_permissions("extras.view_dynamicgroup")
826
+ instance = self.groups[0]
827
+ obj1 = instance.members.first()
828
+ obj_perm = ObjectPermission(
829
+ name="Test permission",
830
+ constraints={"pk__in": [obj1.pk]},
831
+ actions=["view"],
832
+ )
833
+ obj_perm.save()
834
+ obj_perm.users.add(self.user)
835
+ obj_perm.object_types.add(instance.content_type)
836
+
837
+ url = reverse("extras-api:dynamicgroup-members", kwargs={"pk": instance.pk})
838
+ response = self.client.get(url, **self.header)
839
+ self.assertHttpStatus(response, status.HTTP_200_OK)
840
+ self.assertEqual(len(response.json()["results"]), 1)
841
+ self.assertEqual(response.json()["results"][0]["id"], str(obj1.pk))
842
+
821
843
 
822
844
  class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
823
845
  model = DynamicGroupMembership
@@ -5,13 +5,23 @@ from django.test import TestCase
5
5
  from nautobot.core.celery import app
6
6
  from nautobot.core.testing import TransactionTestCase
7
7
  from nautobot.core.utils.lookup import get_changes_for_model
8
- from nautobot.dcim.models import Location, LocationType
8
+ from nautobot.dcim.models import (
9
+ DeviceType,
10
+ DeviceTypeToSoftwareImageFile,
11
+ Location,
12
+ LocationType,
13
+ Manufacturer,
14
+ Platform,
15
+ SoftwareImageFile,
16
+ SoftwareVersion,
17
+ )
9
18
  from nautobot.extras.choices import ObjectChangeActionChoices, ObjectChangeEventContextChoices
10
19
  from nautobot.extras.context_managers import (
11
20
  deferred_change_logging_for_bulk_operation,
12
21
  web_request_context,
13
22
  )
14
23
  from nautobot.extras.models import Status, Webhook
24
+ from nautobot.extras.utils import bulk_delete_with_bulk_change_logging
15
25
 
16
26
  # Use the proper swappable User model
17
27
  User = get_user_model()
@@ -231,6 +241,23 @@ class BulkEditDeleteChangeLogging(TestCase):
231
241
  location.save()
232
242
  location.delete()
233
243
 
244
+ def test_bulk_delete_has_user_in_change_log(self):
245
+ """Test that the bulk delete operation adds the user to the change log"""
246
+ location_type = LocationType.objects.get(name="Campus")
247
+ location_status = Status.objects.get_for_model(Location).first()
248
+ with web_request_context(self.user):
249
+ location = Location(name="Test Location 1", location_type=location_type, status=location_status)
250
+ location.save()
251
+ location_pk = location.pk
252
+ location_qs = Location.objects.filter(pk=location_pk)
253
+ bulk_delete_with_bulk_change_logging(location_qs)
254
+
255
+ oc_list = get_changes_for_model(location)
256
+ self.assertEqual(len(oc_list), 2)
257
+ self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_DELETE)
258
+ self.assertEqual(oc_list[0].user, self.user)
259
+ self.assertEqual(oc_list[0].user_name, self.user.username)
260
+
234
261
  def test_create_then_update(self):
235
262
  """Test that a create followed by an update is logged as a single create"""
236
263
  location_type = LocationType.objects.get(name="Campus")
@@ -276,6 +303,29 @@ class BulkEditDeleteChangeLogging(TestCase):
276
303
  self.assertIsNone(snapshots["differences"]["removed"])
277
304
  self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
278
305
 
306
+ def test_bulk_edit_device_type_software_image_file(self):
307
+ """Test that bulk edits to null does not cause integrity error"""
308
+ manufacturer = Manufacturer.objects.create(name="Test")
309
+ platform = Platform.objects.create(name="Test")
310
+ software_status = Status.objects.get_for_model(SoftwareVersion).first()
311
+ software_version = SoftwareVersion.objects.create(version="1.0.0", platform=platform, status=software_status)
312
+ software_image_file = SoftwareImageFile.objects.create(
313
+ image_file_name="test.iso", software_version=software_version, status=software_status
314
+ )
315
+ device_type = DeviceType.objects.create(manufacturer=manufacturer, model="test123")
316
+ device_type.software_image_files.set([software_image_file])
317
+ with web_request_context(self.user):
318
+ with deferred_change_logging_for_bulk_operation():
319
+ device_type.software_image_files.set([])
320
+ device_type.save()
321
+
322
+ oc_list = get_changes_for_model(DeviceTypeToSoftwareImageFile)
323
+ self.assertEqual(len(oc_list), 1)
324
+ self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_DELETE)
325
+ self.assertIsNotNone(oc_list[0].changed_object_id)
326
+ self.assertEqual(oc_list[0].user, self.user)
327
+ self.assertEqual(oc_list[0].user_name, self.user.username)
328
+
279
329
  def test_change_log_context(self):
280
330
  location_type = LocationType.objects.get(name="Campus")
281
331
  location_status = Status.objects.get_for_model(Location).first()
@@ -30,6 +30,7 @@ from nautobot.extras.constants import HTTP_CONTENT_TYPE_JSON
30
30
  from nautobot.extras.filters import (
31
31
  ComputedFieldFilterSet,
32
32
  ConfigContextFilterSet,
33
+ ContactAssociationFilterSet,
33
34
  ContactFilterSet,
34
35
  ContentTypeFilterSet,
35
36
  CustomFieldChoiceFilterSet,
@@ -555,6 +556,74 @@ class ContactFilterSetTestCase(ContactAndTeamFilterSetTestCaseMixin, FilterTestC
555
556
  )
556
557
 
557
558
 
559
+ class ContactAssociationFilterSetTestCase(FilterTestCases.FilterTestCase):
560
+ queryset = ContactAssociation.objects.all()
561
+ filterset = ContactAssociationFilterSet
562
+
563
+ generic_filter_tests = (
564
+ ["status", "status__id"],
565
+ ["status", "status__name"],
566
+ ["contact", "contact__id"],
567
+ ["contact", "contact__name"],
568
+ ["team", "team__id"],
569
+ ["team", "team__name"],
570
+ ["role", "role__id"],
571
+ ["role", "role__name"],
572
+ )
573
+
574
+ @classmethod
575
+ def setUpTestData(cls):
576
+ roles = Role.objects.get_for_model(ContactAssociation)
577
+ statuses = Status.objects.get_for_model(ContactAssociation)
578
+ ip_addresses = IPAddress.objects.all()
579
+ locations = Location.objects.all()
580
+
581
+ cls.location_ct = ContentType.objects.get_for_model(Location)
582
+ ipaddress_ct = ContentType.objects.get_for_model(IPAddress)
583
+
584
+ ContactAssociation.objects.create(
585
+ contact=Contact.objects.first(),
586
+ associated_object_type=ipaddress_ct,
587
+ associated_object_id=ip_addresses[0].pk,
588
+ role=roles[2],
589
+ status=statuses[1],
590
+ )
591
+ ContactAssociation.objects.create(
592
+ contact=Contact.objects.last(),
593
+ associated_object_type=ipaddress_ct,
594
+ associated_object_id=ip_addresses[1].pk,
595
+ role=roles[1],
596
+ status=statuses[2],
597
+ )
598
+ ContactAssociation.objects.create(
599
+ team=Team.objects.first(),
600
+ associated_object_type=cls.location_ct,
601
+ associated_object_id=locations[0].pk,
602
+ role=roles[3],
603
+ status=statuses[0],
604
+ )
605
+ ContactAssociation.objects.create(
606
+ team=Team.objects.last(),
607
+ associated_object_type=cls.location_ct,
608
+ associated_object_id=locations[1].pk,
609
+ role=roles[0],
610
+ status=statuses[1],
611
+ )
612
+
613
+ def test_associated_object_type(self):
614
+ params = {"associated_object_type": "dcim.location"}
615
+ self.assertEqual(
616
+ self.filterset(params, self.queryset).qs.count(),
617
+ ContactAssociation.objects.filter(associated_object_type=self.location_ct).count(),
618
+ )
619
+
620
+ params = {"associated_object_type": self.location_ct.pk}
621
+ self.assertEqual(
622
+ self.filterset(params, self.queryset).qs.count(),
623
+ ContactAssociation.objects.filter(associated_object_type=self.location_ct).count(),
624
+ )
625
+
626
+
558
627
  class CustomFieldChoiceFilterSetTestCase(FilterTestCases.FilterTestCase):
559
628
  queryset = CustomFieldChoice.objects.all()
560
629
  filterset = CustomFieldChoiceFilterSet
@@ -14,7 +14,6 @@ from nautobot.extras.forms import (
14
14
  ConfigContextFilterForm,
15
15
  ConfigContextForm,
16
16
  CustomFieldModelBulkEditFormMixin,
17
- CustomFieldModelFilterFormMixin,
18
17
  CustomFieldModelFormMixin,
19
18
  JobButtonForm,
20
19
  JobEditForm,
@@ -1089,7 +1088,6 @@ class DeprecatedAliasesTestCase(TestCase):
1089
1088
  AddRemoveTagsForm,
1090
1089
  CustomFieldBulkCreateForm,
1091
1090
  CustomFieldBulkEditForm,
1092
- CustomFieldFilterForm,
1093
1091
  CustomFieldModelForm,
1094
1092
  RelationshipModelForm,
1095
1093
  StatusBulkEditFormMixin,
@@ -1100,7 +1098,6 @@ class DeprecatedAliasesTestCase(TestCase):
1100
1098
  (AddRemoveTagsForm, TagsBulkEditFormMixin),
1101
1099
  (CustomFieldBulkEditForm, CustomFieldModelBulkEditFormMixin),
1102
1100
  (CustomFieldBulkCreateForm, CustomFieldModelBulkEditFormMixin),
1103
- (CustomFieldFilterForm, CustomFieldModelFilterFormMixin),
1104
1101
  (CustomFieldModelForm, CustomFieldModelFormMixin),
1105
1102
  (RelationshipModelForm, RelationshipModelFormMixin),
1106
1103
  (StatusBulkEditFormMixin, StatusModelBulkEditFormMixin),