nautobot 2.3.12__py3-none-any.whl → 2.3.14__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 (324) hide show
  1. nautobot/circuits/tables.py +2 -1
  2. nautobot/core/api/serializers.py +1 -0
  3. nautobot/core/celery/log.py +4 -4
  4. nautobot/core/filters.py +2 -0
  5. nautobot/core/jobs/cleanup.py +47 -11
  6. nautobot/core/models/tree_queries.py +5 -2
  7. nautobot/core/settings.py +1 -1
  8. nautobot/core/tables.py +60 -10
  9. nautobot/core/templates/search.html +7 -0
  10. nautobot/core/templatetags/helpers.py +7 -1
  11. nautobot/core/testing/api.py +5 -1
  12. nautobot/core/testing/filters.py +20 -5
  13. nautobot/core/tests/test_api.py +20 -0
  14. nautobot/core/tests/test_csv.py +25 -3
  15. nautobot/core/tests/test_utils.py +8 -0
  16. nautobot/core/utils/lookup.py +11 -8
  17. nautobot/dcim/api/views.py +3 -0
  18. nautobot/dcim/filters/__init__.py +26 -1
  19. nautobot/dcim/forms.py +10 -5
  20. nautobot/dcim/models/devices.py +1 -0
  21. nautobot/dcim/tests/test_filters.py +33 -0
  22. nautobot/dcim/tests/test_forms.py +51 -2
  23. nautobot/dcim/tests/test_views.py +6 -0
  24. nautobot/extras/api/serializers.py +1 -0
  25. nautobot/extras/api/views.py +2 -0
  26. nautobot/extras/forms/forms.py +2 -0
  27. nautobot/extras/forms/mixins.py +10 -2
  28. nautobot/extras/group_sync.py +3 -3
  29. nautobot/extras/jobs.py +6 -4
  30. nautobot/extras/plugins/__init__.py +13 -2
  31. nautobot/extras/tests/test_views.py +2 -0
  32. nautobot/ipam/lookups.py +101 -62
  33. nautobot/ipam/models.py +62 -11
  34. nautobot/ipam/tables.py +20 -6
  35. nautobot/ipam/tests/test_api.py +68 -1
  36. nautobot/ipam/tests/test_models.py +41 -0
  37. nautobot/ipam/tests/test_querysets.py +49 -1
  38. nautobot/ipam/utils/__init__.py +24 -0
  39. nautobot/ipam/views.py +61 -68
  40. nautobot/project-static/docs/404.html +1 -1
  41. nautobot/project-static/docs/apps/index.html +1 -1
  42. nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
  43. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1 -1
  44. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1 -1
  45. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
  46. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +1 -1
  47. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1 -1
  48. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1 -1
  49. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +1 -1
  50. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1 -1
  51. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +1 -1
  52. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1 -1
  53. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1 -1
  54. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
  55. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1 -1
  56. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +7 -5
  57. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1 -1
  58. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +1 -1
  59. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1 -1
  60. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +197 -5
  61. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +2 -2
  62. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1 -1
  63. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1 -1
  64. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +16 -2
  65. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1 -1
  66. nautobot/project-static/docs/development/apps/api/configuration-view.html +1 -1
  67. nautobot/project-static/docs/development/apps/api/database-backend-config.html +1 -1
  68. nautobot/project-static/docs/development/apps/api/models/django-admin.html +1 -1
  69. nautobot/project-static/docs/development/apps/api/models/global-search.html +1 -1
  70. nautobot/project-static/docs/development/apps/api/models/graphql.html +1 -1
  71. nautobot/project-static/docs/development/apps/api/models/index.html +1 -1
  72. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +1 -1
  73. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +1 -1
  74. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +1 -1
  75. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +1 -1
  76. nautobot/project-static/docs/development/apps/api/platform-features/index.html +1 -1
  77. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +1 -1
  78. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
  79. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +1 -1
  80. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +1 -1
  81. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +23 -4
  82. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +1 -1
  83. nautobot/project-static/docs/development/apps/api/prometheus.html +1 -1
  84. nautobot/project-static/docs/development/apps/api/setup.html +1 -1
  85. nautobot/project-static/docs/development/apps/api/testing.html +1 -1
  86. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +1 -1
  87. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +1 -1
  88. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +1 -1
  89. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +1 -1
  90. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +1 -1
  91. nautobot/project-static/docs/development/apps/api/views/base-template.html +1 -1
  92. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +1 -1
  93. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +1 -1
  94. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +1 -1
  95. nautobot/project-static/docs/development/apps/api/views/index.html +1 -1
  96. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -1
  97. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +1 -1
  98. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +1 -1
  99. nautobot/project-static/docs/development/apps/api/views/notes.html +1 -1
  100. nautobot/project-static/docs/development/apps/api/views/rest-api.html +1 -1
  101. nautobot/project-static/docs/development/apps/api/views/urls.html +1 -1
  102. nautobot/project-static/docs/development/apps/index.html +1 -1
  103. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  104. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
  105. nautobot/project-static/docs/development/apps/migration/from-v1.html +1 -1
  106. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +1 -1
  107. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +1 -1
  108. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +1 -1
  109. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +1 -1
  110. nautobot/project-static/docs/development/apps/porting-from-netbox.html +1 -1
  111. nautobot/project-static/docs/development/core/application-registry.html +1 -1
  112. nautobot/project-static/docs/development/core/best-practices.html +1 -1
  113. nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
  114. nautobot/project-static/docs/development/core/caching.html +1 -1
  115. nautobot/project-static/docs/development/core/controllers.html +1 -1
  116. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +1 -1
  117. nautobot/project-static/docs/development/core/generic-views.html +1 -1
  118. nautobot/project-static/docs/development/core/getting-started.html +154 -140
  119. nautobot/project-static/docs/development/core/homepage.html +1 -1
  120. nautobot/project-static/docs/development/core/index.html +1 -1
  121. nautobot/project-static/docs/development/core/model-checklist.html +1 -1
  122. nautobot/project-static/docs/development/core/model-features.html +1 -1
  123. nautobot/project-static/docs/development/core/natural-keys.html +1 -1
  124. nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
  125. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  126. nautobot/project-static/docs/development/core/role-internals.html +1 -1
  127. nautobot/project-static/docs/development/core/settings.html +1 -1
  128. nautobot/project-static/docs/development/core/style-guide.html +1 -1
  129. nautobot/project-static/docs/development/core/templates.html +1 -1
  130. nautobot/project-static/docs/development/core/testing.html +1 -1
  131. nautobot/project-static/docs/development/core/user-preferences.html +1 -1
  132. nautobot/project-static/docs/development/index.html +1 -1
  133. nautobot/project-static/docs/development/jobs/index.html +142 -118
  134. nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
  135. nautobot/project-static/docs/index.html +1 -1
  136. nautobot/project-static/docs/objects.inv +0 -0
  137. nautobot/project-static/docs/overview/application_stack.html +1 -1
  138. nautobot/project-static/docs/overview/design_philosophy.html +1 -1
  139. nautobot/project-static/docs/release-notes/index.html +1 -1
  140. nautobot/project-static/docs/release-notes/version-1.0.html +1 -1
  141. nautobot/project-static/docs/release-notes/version-1.1.html +1 -1
  142. nautobot/project-static/docs/release-notes/version-1.2.html +1 -1
  143. nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
  144. nautobot/project-static/docs/release-notes/version-1.4.html +1 -1
  145. nautobot/project-static/docs/release-notes/version-1.5.html +1 -1
  146. nautobot/project-static/docs/release-notes/version-1.6.html +614 -179
  147. nautobot/project-static/docs/release-notes/version-2.0.html +1 -1
  148. nautobot/project-static/docs/release-notes/version-2.1.html +1 -1
  149. nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
  150. nautobot/project-static/docs/release-notes/version-2.3.html +553 -200
  151. nautobot/project-static/docs/requirements.txt +1 -1
  152. nautobot/project-static/docs/search/search_index.json +1 -1
  153. nautobot/project-static/docs/sitemap.xml +270 -270
  154. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  155. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +1 -1
  156. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +1 -1
  157. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +3 -3
  158. nautobot/project-static/docs/user-guide/administration/configuration/index.html +1 -1
  159. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
  160. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +1 -1
  161. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +1 -1
  162. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +1 -1
  163. nautobot/project-static/docs/user-guide/administration/guides/docker.html +1 -1
  164. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
  165. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +1 -1
  166. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +1 -1
  167. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +1 -1
  168. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +1 -1
  169. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +1 -1
  170. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +1 -1
  171. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
  172. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +1 -1
  173. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +1 -1
  174. nautobot/project-static/docs/user-guide/administration/installation/index.html +1 -1
  175. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
  176. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -1
  177. nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
  178. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +1 -1
  179. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +1 -1
  180. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
  181. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
  182. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +1 -1
  183. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +1 -1
  184. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +1 -1
  185. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +1 -1
  186. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +1 -1
  187. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +1 -1
  188. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +1 -1
  189. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  190. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +1 -1
  191. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +1 -1
  192. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +1 -1
  193. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +1 -1
  194. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +1 -1
  195. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +1 -1
  196. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +1 -1
  197. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +1 -1
  198. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +1 -1
  199. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +1 -1
  200. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +1 -1
  201. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +1 -1
  202. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +1 -1
  203. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +1 -1
  204. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +1 -1
  205. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +1 -1
  206. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +1 -1
  207. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +1 -1
  208. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +1 -1
  209. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +1 -1
  210. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +1 -1
  211. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +1 -1
  212. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +1 -1
  213. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +1 -1
  214. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +1 -1
  215. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +1 -1
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +1 -1
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +1 -1
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -1
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +1 -1
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +1 -1
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +1 -1
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +1 -1
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -1
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +1 -1
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +1 -1
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +1 -1
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +1 -1
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +1 -1
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +1 -1
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +1 -1
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +1 -1
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +1 -1
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +1 -1
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +1 -1
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +1 -1
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +1 -1
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +1 -1
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +1 -1
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +1 -1
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +1 -1
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +1 -1
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +1 -1
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +1 -1
  244. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +1 -1
  245. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +1 -1
  246. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +1 -1
  247. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +1 -1
  248. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +1 -1
  249. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +1 -1
  250. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +1 -1
  251. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +1 -1
  252. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +1 -1
  253. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +1 -1
  254. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +1 -1
  255. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +1 -1
  256. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +1 -1
  257. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +1 -1
  258. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +1 -1
  259. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +1 -1
  260. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +1 -1
  261. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +1 -1
  262. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +1 -1
  263. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +1 -1
  264. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +1 -1
  265. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +1 -1
  266. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +1 -1
  267. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +1 -1
  268. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -1
  269. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +1 -1
  270. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +1 -1
  271. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +1 -1
  272. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
  273. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +1 -1
  274. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
  275. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +1 -1
  276. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  277. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +1 -1
  278. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +1 -1
  279. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +1 -1
  280. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +1 -1
  281. nautobot/project-static/docs/user-guide/index.html +1 -1
  282. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +1 -1
  283. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +1 -1
  284. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +1 -1
  285. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +1 -1
  286. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
  287. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +1 -1
  288. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +1 -1
  289. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +1 -1
  290. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +1 -1
  291. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +1 -1
  292. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +1 -1
  293. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +1 -1
  294. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +1 -1
  295. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +1 -1
  296. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +1 -1
  297. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +1 -1
  298. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +1 -1
  299. nautobot/project-static/docs/user-guide/platform-functionality/note.html +1 -1
  300. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +1 -1
  301. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -1
  302. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +1 -1
  303. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +1 -1
  304. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +1 -1
  305. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +1 -1
  306. nautobot/project-static/docs/user-guide/platform-functionality/role.html +1 -1
  307. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +1 -1
  308. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +1 -1
  309. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +1 -1
  310. nautobot/project-static/docs/user-guide/platform-functionality/status.html +1 -1
  311. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +1 -1
  312. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -1
  313. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +1 -1
  314. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +1 -1
  315. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +1 -1
  316. nautobot/users/api/serializers.py +1 -0
  317. nautobot/virtualization/filters.py +19 -2
  318. nautobot/virtualization/tests/test_filters.py +9 -0
  319. {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/METADATA +3 -3
  320. {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/RECORD +324 -324
  321. {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/LICENSE.txt +0 -0
  322. {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/NOTICE +0 -0
  323. {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/WHEEL +0 -0
  324. {nautobot-2.3.12.dist-info → nautobot-2.3.14.dist-info}/entry_points.txt +0 -0
@@ -110,6 +110,7 @@ class CircuitTable(StatusTableMixin, BaseTable):
110
110
  pk = ToggleColumn()
111
111
  cid = tables.LinkColumn(verbose_name="ID")
112
112
  provider = tables.Column(linkify=True)
113
+ circuit_type = tables.Column(linkify=True)
113
114
  tenant = TenantColumn()
114
115
  tags = TagColumn(url_name="circuits:circuit_list")
115
116
 
@@ -146,7 +147,7 @@ class CircuitTable(StatusTableMixin, BaseTable):
146
147
  "pk",
147
148
  "cid",
148
149
  "provider",
149
- "type",
150
+ "circuit_type",
150
151
  "status",
151
152
  "tenant",
152
153
  "circuit_termination_a",
@@ -134,6 +134,7 @@ class BaseModelSerializer(OptInFieldsMixin, serializers.HyperlinkedModelSerializ
134
134
 
135
135
  serializer_related_field = NautobotHyperlinkedRelatedField
136
136
 
137
+ id = serializers.UUIDField(read_only=False, default=serializers.CreateOnlyDefault(uuid.uuid4))
137
138
  display = serializers.SerializerMethodField(read_only=True, help_text="Human friendly display value")
138
139
  object_type = ObjectTypeField()
139
140
  # composite_key = serializers.SerializerMethodField() # TODO: Revisit if we reintroduce composite keys
@@ -11,6 +11,10 @@ class NautobotDatabaseHandler(logging.Handler):
11
11
  if current_task is None:
12
12
  return
13
13
 
14
+ # Skip recording the log entry if it has been marked as such
15
+ if getattr(record, "skip_db_logging", False):
16
+ return
17
+
14
18
  from nautobot.extras.models.jobs import JobResult
15
19
 
16
20
  try:
@@ -24,10 +28,6 @@ class NautobotDatabaseHandler(logging.Handler):
24
28
  # JobResult.DoesNotExist - because we might not have a JobResult with that ID
25
29
  return
26
30
 
27
- # Skip recording the log entry if it has been marked as such
28
- if getattr(record, "skip_db_logging", False):
29
- return
30
-
31
31
  job_result.log(
32
32
  message=record.message,
33
33
  level_choice=record.levelname.lower(),
nautobot/core/filters.py CHANGED
@@ -618,6 +618,7 @@ class BaseFilterSet(django_filters.FilterSet):
618
618
  @staticmethod
619
619
  def _get_filter_lookup_dict(existing_filter):
620
620
  # Choose the lookup expression map based on the filter type
621
+
621
622
  if isinstance(
622
623
  existing_filter,
623
624
  (
@@ -637,6 +638,7 @@ class BaseFilterSet(django_filters.FilterSet):
637
638
  (
638
639
  django_filters.ModelChoiceFilter,
639
640
  django_filters.ModelMultipleChoiceFilter,
641
+ MultiValueUUIDFilter,
640
642
  TagFilter,
641
643
  TreeNodeMultipleChoiceFilter,
642
644
  ),
@@ -1,6 +1,7 @@
1
1
  from datetime import timedelta
2
2
 
3
3
  from django.core.exceptions import PermissionDenied
4
+ from django.db.models import CASCADE
4
5
  from django.db.models.signals import pre_delete
5
6
  from django.utils import timezone
6
7
 
@@ -48,6 +49,27 @@ class LogsCleanup(Job):
48
49
  description = "Delete ObjectChange and/or JobResult/JobLogEntry records older than a specified cutoff."
49
50
  has_sensitive_variables = False
50
51
 
52
+ def recursive_delete_with_cascade(self, queryset, deletion_summary):
53
+ """
54
+ Recursively deletes all related objects with CASCADE for a given queryset.
55
+
56
+ Args:
57
+ queryset (QuerySet): The queryset of objects to delete.
58
+ deletion_summary (dict): A dictionary to store the count of deleted objects for each model.
59
+ """
60
+ related_objects = queryset.model._meta.related_objects
61
+ queryset = queryset.only("id")
62
+
63
+ for related_object in related_objects:
64
+ if related_object.on_delete is CASCADE:
65
+ related_model = related_object.related_model
66
+ related_field_name = related_object.field.name
67
+ cascade_queryset = related_model.objects.filter(**{f"{related_field_name}__id__in": queryset})
68
+ self.recursive_delete_with_cascade(cascade_queryset, deletion_summary)
69
+ _, deleted_dict = queryset.delete()
70
+ deletion_summary.update(deleted_dict)
71
+ return deletion_summary
72
+
51
73
  def run(self, *, cleanup_types, max_age=None):
52
74
  if max_age in (None, ""):
53
75
  max_age = get_settings_or_config("CHANGELOG_RETENTION")
@@ -77,22 +99,36 @@ class LogsCleanup(Job):
77
99
 
78
100
  if CleanupTypes.JOB_RESULT in cleanup_types:
79
101
  self.logger.info("Deleting JobResult records prior to %s", cutoff)
80
- _, deleted_dict = JobResult.objects.restrict(self.user, "delete").filter(date_done__lt=cutoff).delete()
102
+ queryset = JobResult.objects.restrict(self.user, "delete").filter(date_done__lt=cutoff)
103
+ deletion_summary = {}
104
+ self.recursive_delete_with_cascade(queryset, deletion_summary)
81
105
  result.setdefault("extras.JobResult", 0)
82
106
  result.setdefault("extras.JobLogEntry", 0)
83
- result.update(**deleted_dict)
84
- self.logger.info(
85
- "Deleted %d JobResult records and their associated %d JobLogEntry records",
86
- result["extras.JobResult"],
87
- result["extras.JobLogEntry"],
88
- )
107
+ result.update(deletion_summary)
108
+
109
+ for modelname, count in deletion_summary.items():
110
+ self.logger.info(
111
+ "As part of deleting %d JobResult records, also deleted %d related %s records",
112
+ result["extras.JobResult"],
113
+ count,
114
+ modelname,
115
+ )
89
116
 
90
117
  if CleanupTypes.OBJECT_CHANGE in cleanup_types:
91
118
  self.logger.info("Deleting ObjectChange records prior to %s", cutoff)
92
- deleted_count, _ = ObjectChange.objects.restrict(self.user, "delete").filter(time__lt=cutoff).delete()
93
- self.logger.info("Deleted %d ObjectChange records", deleted_count)
94
- result["extras.ObjectChange"] = deleted_count
95
-
119
+ queryset = ObjectChange.objects.restrict(self.user, "delete").filter(time__lt=cutoff)
120
+ deletion_summary = {}
121
+ self.recursive_delete_with_cascade(queryset, deletion_summary)
122
+ result.setdefault("extras.ObjectChange", 0)
123
+ result.update(deletion_summary)
124
+
125
+ for modelname, count in deletion_summary.items():
126
+ self.logger.info(
127
+ "As part of deleting %d ObjectChange records, also deleted %d related %s records",
128
+ result["extras.ObjectChange"],
129
+ count,
130
+ modelname,
131
+ )
96
132
  return result
97
133
  finally:
98
134
  # Be sure to clean up after ourselves!
@@ -111,8 +111,11 @@ class TreeModel(TreeNode):
111
111
  if display_str:
112
112
  return display_str
113
113
  try:
114
- if self.parent is not None:
115
- display_str = self.parent.display + ""
114
+ if self.parent_id is not None:
115
+ parent_display_str = cache.get(cache_key.replace(str(self.id), str(self.parent_id)), "")
116
+ if not parent_display_str:
117
+ parent_display_str = self.parent.display
118
+ display_str = parent_display_str + " → "
116
119
  except self.DoesNotExist:
117
120
  # Expected to occur at times during bulk-delete operations
118
121
  pass
nautobot/core/settings.py CHANGED
@@ -225,7 +225,7 @@ if SSO_ENABLE_GROUP_SYNC:
225
225
  "social_core.pipeline.social_auth.associate_user",
226
226
  "social_core.pipeline.social_auth.load_extra_data",
227
227
  "social_core.pipeline.user.user_details",
228
- "nautobot.extras.group_sync",
228
+ "nautobot.extras.group_sync.group_sync",
229
229
  )
230
230
  # OAuth2/OIDC claim where the list of groups the authenticating user is a part of
231
231
  SSO_CLAIMS_GROUP = os.getenv("NAUTOBOT_SSO_CLAIMS_GROUP", "groups")
nautobot/core/tables.py CHANGED
@@ -14,7 +14,8 @@ from django.utils.http import urlencode
14
14
  from django.utils.safestring import mark_safe
15
15
  from django.utils.text import Truncator
16
16
  import django_tables2
17
- from django_tables2.data import TableQuerysetData
17
+ from django_tables2.data import TableData, TableQuerysetData
18
+ from django_tables2.rows import BoundRows
18
19
  from django_tables2.utils import Accessor, OrderBy, OrderByTuple
19
20
  from tree_queries.models import TreeNode
20
21
 
@@ -46,8 +47,24 @@ class BaseTable(django_tables2.Table):
46
47
  user=None,
47
48
  hide_hierarchy_ui=False,
48
49
  order_by=None,
50
+ data_transform_callback=None,
49
51
  **kwargs,
50
52
  ):
53
+ """
54
+ Instantiate a BaseTable.
55
+
56
+ Args:
57
+ *args (list, optional): Passed through to django_tables2.Table
58
+ table_changes_pending (bool): TODO
59
+ saved_view (SavedView, optional): TODO
60
+ user (User, optional): TODO
61
+ hide_hierarchy_ui (bool): Whether to display or hide hierarchy indentation of nested objects.
62
+ order_by (list, optional): Field(s) to sort by
63
+ data_transform_callback (function, optional): A function that takes the given `data` as an input and
64
+ returns new data. Runs after all of the queryset auto-optimization performed by this class.
65
+ Used for example in IPAM views to inject "fake" records for "available" Prefixes, IPAddresses, or VLANs.
66
+ **kwargs (dict, optional): Passed through to django_tables2.Table
67
+ """
51
68
  # Add custom field columns
52
69
  model = self._meta.model
53
70
 
@@ -89,6 +106,14 @@ class BaseTable(django_tables2.Table):
89
106
  # Init table
90
107
  super().__init__(*args, order_by=order_by, **kwargs)
91
108
 
109
+ if not isinstance(self.data, TableQuerysetData):
110
+ # LinkedCountColumns don't work properly if the data is a list of dicts instead of a queryset,
111
+ # as they rely on a `queryset.annotate()` call to gather the data.
112
+ self.exclude = [
113
+ *self.exclude,
114
+ *[column.name for column in self.columns if isinstance(column.column, LinkedCountColumn)],
115
+ ]
116
+
92
117
  # Don't show hierarchy if we're sorted
93
118
  if order_by is not None and hide_hierarchy_ui is None:
94
119
  hide_hierarchy_ui = True
@@ -122,7 +147,7 @@ class BaseTable(django_tables2.Table):
122
147
  columns = user.get_config(f"tables.{self.__class__.__name__}.columns")
123
148
  if columns:
124
149
  for name, column in self.base_columns.items():
125
- if name in columns:
150
+ if name in columns and name not in self.exclude:
126
151
  self.columns.show(name)
127
152
  else:
128
153
  self.columns.hide(name)
@@ -161,7 +186,8 @@ class BaseTable(django_tables2.Table):
161
186
  logger.error("Couldn't find model for %s", column.column.viewname)
162
187
  continue
163
188
  reverse_lookup = column.column.reverse_lookup or next(iter(column.column.url_params.keys()))
164
- count_fields.append((column.name, column_model, reverse_lookup))
189
+ distinct = column.column.distinct
190
+ count_fields.append((column.name, column_model, reverse_lookup, distinct))
165
191
  try:
166
192
  lookup = column.column.lookup or get_related_field_for_models(model, column_model).name
167
193
  # For some reason get_related_field_for_models(Tag, DynamicGroup) gives a M2M with the name
@@ -173,7 +199,8 @@ class BaseTable(django_tables2.Table):
173
199
  if lookup is not None:
174
200
  # Also attempt to prefetch the first matching record for display - see LinkedCountColumn
175
201
  prefetch_fields.append(
176
- Prefetch(lookup, column_model.objects.all()[:1], to_attr=f"{lookup}_list")
202
+ # Use order_by() because we don't care about ordering here and it's potentially expensive
203
+ Prefetch(lookup, column_model.objects.order_by()[:1], to_attr=f"{lookup}_list")
177
204
  )
178
205
  continue
179
206
 
@@ -256,24 +283,34 @@ class BaseTable(django_tables2.Table):
256
283
  )
257
284
 
258
285
  if count_fields:
259
- for column_name, column_model, lookup_name in count_fields:
286
+ for column_name, column_model, lookup_name, distinct in count_fields:
260
287
  if hasattr(queryset.first(), column_name):
261
288
  continue
262
289
  try:
263
290
  logger.debug(
264
- "Applying .annotate(%s=count_related(%s, %r) to %s QuerySet",
291
+ "Applying .annotate(%s=count_related(%s, %r, distinct=%s) to %s QuerySet",
265
292
  column_name,
266
293
  column_model.__name__,
267
294
  lookup_name,
295
+ distinct,
268
296
  model.__name__,
269
297
  )
270
- queryset = queryset.annotate(**{column_name: count_related(column_model, lookup_name)})
298
+ queryset = queryset.annotate(
299
+ **{column_name: count_related(column_model, lookup_name, distinct=distinct)}
300
+ )
271
301
  except FieldError:
272
302
  # No error message logged here as the above is *very much* best-effort
273
303
  pass
274
304
 
275
305
  self.data.data = queryset
276
306
 
307
+ # TODO: it would be better if we could apply this transformation and the above queryset optimizations
308
+ # **before** calling super().__init__(), but the current implementation works for now, though inelegant.
309
+ if data_transform_callback is not None:
310
+ self.data = TableData.from_data(data_transform_callback(self.data.data))
311
+ self.data.set_table(self)
312
+ self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data)
313
+
277
314
  @property
278
315
  def configurable_columns(self):
279
316
  selected_columns = [
@@ -288,7 +325,7 @@ class BaseTable(django_tables2.Table):
288
325
 
289
326
  @property
290
327
  def visible_columns(self):
291
- return [name for name in self.sequence if self.columns[name].visible]
328
+ return [name for name in self.sequence if self.columns[name].visible and name not in self.exclude]
292
329
 
293
330
  @property
294
331
  def order_by(self):
@@ -490,6 +527,7 @@ class LinkedCountColumn(django_tables2.Column):
490
527
  TODO: this currently does *not* support nested lookups via `__`. That may be solvable in the future.
491
528
  reverse_lookup (str, optional): The reverse lookup parameter to use to derive the count.
492
529
  If not specified, the first key in `url_params` will be implicitly used as the `reverse_lookup` value.
530
+ distinct (bool, optional): Parameter passed through to `count_related()`.
493
531
  **kwargs (dict, optional): As the parent Column class.
494
532
 
495
533
  Examples:
@@ -514,21 +552,33 @@ class LinkedCountColumn(django_tables2.Column):
514
552
  # We'd like to do the below but this module isn't currently smart enough to build the right Prefetch()
515
553
  # for a nested lookup:
516
554
  # lookup="circuit_terminations__circuit",
517
- # For the count, .annotate(circuit_count=count_related(Circuit, "circuit_terminations__cloud_network"))
555
+ # For the count,
556
+ # .annotate(circuit_count=count_related(Circuit, "circuit_terminations__cloud_network", distinct=True))
518
557
  reverse_lookup="circuit_terminations__cloud_network",
558
+ distinct=True,
519
559
  verbose_name="Circuits",
520
560
  )
521
561
  ```
522
562
  """
523
563
 
524
564
  def __init__(
525
- self, viewname, *args, view_kwargs=None, url_params=None, lookup=None, reverse_lookup=None, default=0, **kwargs
565
+ self,
566
+ viewname,
567
+ *args,
568
+ view_kwargs=None,
569
+ url_params=None,
570
+ lookup=None,
571
+ reverse_lookup=None,
572
+ distinct=False,
573
+ default=None,
574
+ **kwargs,
526
575
  ):
527
576
  self.viewname = viewname
528
577
  self.lookup = lookup
529
578
  self.view_kwargs = view_kwargs or {}
530
579
  self.url_params = url_params
531
580
  self.reverse_lookup = reverse_lookup or next(iter(url_params.keys()))
581
+ self.distinct = distinct
532
582
  self.model = get_model_for_view_name(self.viewname)
533
583
  super().__init__(*args, default=default, **kwargs)
534
584
 
@@ -65,3 +65,10 @@
65
65
  </div>
66
66
  {% endif %}
67
67
  {% endblock %}
68
+
69
+ {% block javascript %}
70
+ {{ block.super }}
71
+ <script>
72
+ var clipboard = new ClipboardJS('.btn');
73
+ </script>
74
+ {% endblock %}
@@ -795,7 +795,13 @@ def saved_view_modal(
795
795
  "clear_view",
796
796
  ]
797
797
 
798
- table_name = lookup.get_table_for_model(model).__name__
798
+ view_class = lookup.get_view_for_model(model, "List")
799
+ table_name = None
800
+ if hasattr(view_class, "table"):
801
+ table_name = view_class.table.__name__
802
+ if hasattr(view_class, "table_class"):
803
+ table_name = view_class.table_class.__name__
804
+
799
805
  for param in non_filter_params:
800
806
  if param == "saved_view":
801
807
  current_saved_view_pk = filters_applied.pop(param, None)
@@ -707,7 +707,11 @@ class APIViewTestCases:
707
707
  self.assertHttpStatus(response, status.HTTP_201_CREATED, csv_data)
708
708
  # Note that create via CSV is always treated as a bulk-create, and so the response is always a list of dicts
709
709
  new_instance = self._get_queryset().get(pk=response.data[0]["id"])
710
- self.assertNotEqual(new_instance.pk, orig_pk)
710
+ if isinstance(orig_pk, int):
711
+ self.assertNotEqual(new_instance.pk, orig_pk)
712
+ else:
713
+ # for our non-integer PKs, we're expecting the creation to respect the requested PK
714
+ self.assertEqual(new_instance.pk, orig_pk)
711
715
 
712
716
  new_serializer = serializer_class(new_instance, context={"request": None})
713
717
  new_data = new_serializer.data
@@ -88,11 +88,26 @@ class FilterTestCases:
88
88
  return self.filterset.declared_filters["q"].filter_predicates
89
89
 
90
90
  def test_id(self):
91
- """Verify that the filterset supports filtering by id."""
92
- params = {"id": self.queryset.values_list("pk", flat=True)[:2]}
93
- filterset = self.filterset(params, self.queryset)
94
- self.assertTrue(filterset.is_valid())
95
- self.assertEqual(filterset.qs.count(), 2)
91
+ """Verify that the filterset supports filtering by id with only lookup `__n`."""
92
+ with self.subTest("Assert `id`"):
93
+ params = {"id": list(self.queryset.values_list("pk", flat=True)[:2])}
94
+ expected_queryset = self.queryset.filter(id__in=params["id"])
95
+ filterset = self.filterset(params, self.queryset)
96
+ self.assertTrue(filterset.is_valid())
97
+ self.assertQuerysetEqualAndNotEmpty(filterset.qs.order_by("id"), expected_queryset.order_by("id"))
98
+
99
+ with self.subTest("Assert negate lookup"):
100
+ params = {"id__n": list(self.queryset.values_list("pk", flat=True)[:2])}
101
+ expected_queryset = self.queryset.exclude(id__in=params["id__n"])
102
+ filterset = self.filterset(params, self.queryset)
103
+ self.assertTrue(filterset.is_valid())
104
+ self.assertQuerysetEqualAndNotEmpty(filterset.qs.order_by("id"), expected_queryset.order_by("id"))
105
+
106
+ with self.subTest("Assert invalid lookup"):
107
+ params = {"id__in": list(self.queryset.values_list("pk", flat=True)[:2])}
108
+ filterset = self.filterset(params, self.queryset)
109
+ self.assertFalse(filterset.is_valid())
110
+ self.assertIn("Unknown filter field", filterset.errors.as_text())
96
111
 
97
112
  def test_invalid_filter(self):
98
113
  """Verify that the filterset reports as invalid when initialized with an unsupported filter parameter."""
@@ -3,6 +3,7 @@ from io import BytesIO, StringIO
3
3
  import json
4
4
  import os
5
5
  from unittest import skip
6
+ import uuid
6
7
 
7
8
  from constance import config
8
9
  from constance.test import override_config
@@ -754,6 +755,25 @@ class WritableNestedSerializerTest(testing.APITestCase):
754
755
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
755
756
  self.assertEqual(ipam_models.VLAN.objects.filter(name="Test VLAN 100").count(), 0)
756
757
 
758
+ def test_create_with_specified_id(self):
759
+ data = {
760
+ "id": str(uuid.uuid4()),
761
+ "vid": 400,
762
+ "name": "Test VLAN 400",
763
+ "status": self.statuses.first().pk,
764
+ "vlan_group": self.vlan_group1.pk,
765
+ }
766
+ url = reverse("ipam-api:vlan-list")
767
+ self.add_permissions("ipam.add_vlan")
768
+
769
+ response = self.client.post(url, data, format="json", **self.header)
770
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
771
+ self.assertEqual(str(response.data["vlan_group"]["url"]), self.absolute_api_url(self.vlan_group1))
772
+ self.assertEqual(str(response.data["id"]), data["id"])
773
+ vlan = ipam_models.VLAN.objects.get(pk=response.data["id"])
774
+ self.assertEqual(vlan.status, self.statuses.first())
775
+ self.assertEqual(vlan.vlan_group, self.vlan_group1)
776
+
757
777
 
758
778
  class APIOrderingTestCase(testing.APITestCase):
759
779
  """
@@ -1,3 +1,6 @@
1
+ import csv
2
+ import io
3
+
1
4
  from django.contrib.contenttypes.models import ContentType
2
5
  from django.test import override_settings, RequestFactory, TestCase
3
6
  from django.urls import reverse
@@ -261,9 +264,28 @@ class CSVParsingRelatedTestCase(TestCase):
261
264
  self.assertEqual(response.status_code, 200)
262
265
  response_data = response.content.decode(response.charset)
263
266
 
264
- # Replace Device Name
265
- import_data = response_data.replace("TestDevice1", "TestDevice3").replace("TestDevice2", "")
266
- data = {"csv_data": import_data}
267
+ # parse the csv data
268
+ csv_reader = csv.DictReader(response_data.splitlines())
269
+ # remove the 'id' column so that all the items are imported new
270
+ fieldnames = [field for field in csv_reader.fieldnames if field != "id"]
271
+ # read all entries into a list
272
+ response_csv = list(csv_reader)
273
+
274
+ # mutate our data for testing purposes
275
+ for row in response_csv:
276
+ if row["name"] == "TestDevice1":
277
+ row["name"] = "TestDevice3"
278
+ elif row["name"] == "TestDevice2":
279
+ row["name"] = ""
280
+
281
+ # prep our data to write out
282
+ with io.StringIO() as import_csv:
283
+ writer = csv.DictWriter(import_csv, fieldnames=fieldnames)
284
+ writer.writeheader()
285
+ for row in response_csv:
286
+ filtered_row = {key: row[key] for key in fieldnames}
287
+ writer.writerow(filtered_row)
288
+ data = {"csv_data": import_csv.getvalue()}
267
289
  url = reverse("dcim:device_import")
268
290
  response = self.client.post(url, data)
269
291
  self.assertEqual(response.status_code, 200)
@@ -268,6 +268,14 @@ class GetFooForModelTest(TestCase):
268
268
  lookup.get_model_for_view_name("unknown:plugins:example_app:examplemodel_list")
269
269
  self.assertEqual(str(err.exception), "Unexpected View Name: unknown:plugins:example_app:examplemodel_list")
270
270
 
271
+ def test_get_table_class_string_from_view_name(self):
272
+ # Testing UIViewSet
273
+ self.assertEqual(lookup.get_table_class_string_from_view_name("circuits:circuit_list"), "CircuitTable")
274
+ # Testing Legacy View
275
+ self.assertEqual(lookup.get_table_class_string_from_view_name("dcim:location_list"), "LocationTable")
276
+ # Testing unconventional table name
277
+ self.assertEqual(lookup.get_table_class_string_from_view_name("ipam:prefix_list"), "PrefixDetailTable")
278
+
271
279
 
272
280
  class IsTaggableTest(TestCase):
273
281
  def test_is_taggable_true(self):
@@ -8,7 +8,7 @@ from django.conf import settings
8
8
  from django.contrib.auth.models import Group
9
9
  from django.contrib.contenttypes.models import ContentType
10
10
  from django.db.models import Model
11
- from django.urls import get_resolver, URLPattern, URLResolver
11
+ from django.urls import get_resolver, resolve, reverse, URLPattern, URLResolver
12
12
  from django.utils.module_loading import import_string
13
13
 
14
14
 
@@ -212,7 +212,7 @@ def get_related_field_for_models(from_model, to_model):
212
212
  return matching_field
213
213
 
214
214
 
215
- def get_table_for_model(model):
215
+ def get_table_for_model(model, suffix=None):
216
216
  """Return the `Table` class associated with a given `model`.
217
217
 
218
218
  The `Table` class is expected to be in the `tables` module within the application
@@ -222,11 +222,12 @@ def get_table_for_model(model):
222
222
 
223
223
  Args:
224
224
  model (BaseModel): A model class
225
+ suffix (str): A replacement suffix for the table name (e.g. `DetailTable`, such as to retrieve `FooDetailTable`)
225
226
 
226
227
  Returns:
227
228
  (Union[Table, None]): Either the `Table` class or `None`
228
229
  """
229
- return get_related_class_for_model(model, module_name="tables", object_suffix="Table")
230
+ return get_related_class_for_model(model, module_name="tables", object_suffix=suffix or "Table")
230
231
 
231
232
 
232
233
  def get_view_for_model(model, view_type=""):
@@ -275,11 +276,13 @@ def get_table_class_string_from_view_name(view_name):
275
276
  Returns:
276
277
  table_class_name (String): The name of the model table class or None e.g. LocationTable, CircuitTable
277
278
  """
278
- model = get_model_for_view_name(view_name)
279
- if model:
280
- table_class = get_table_for_model(model)
281
- if table_class:
282
- return table_class.__name__
279
+
280
+ view_func = resolve(reverse(view_name)).func
281
+ view_class = getattr(view_func, "cls", getattr(view_func, "view_class", None))
282
+ if hasattr(view_class, "table_class"):
283
+ return view_class.table_class.__name__
284
+ if hasattr(view_class, "table"):
285
+ return view_class.table.__name__
283
286
  return None
284
287
 
285
288
 
@@ -11,6 +11,7 @@ from drf_spectacular.types import OpenApiTypes
11
11
  from drf_spectacular.utils import extend_schema, OpenApiParameter
12
12
  from rest_framework.decorators import action
13
13
  from rest_framework.mixins import ListModelMixin
14
+ from rest_framework.parsers import JSONParser, MultiPartParser
14
15
  from rest_framework.permissions import IsAuthenticated
15
16
  from rest_framework.response import Response
16
17
  from rest_framework.viewsets import GenericViewSet, ViewSet
@@ -18,6 +19,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
18
19
  from nautobot.circuits.models import Circuit
19
20
  from nautobot.cloud.models import CloudAccount
20
21
  from nautobot.core.api.exceptions import ServiceUnavailable
22
+ from nautobot.core.api.parsers import NautobotCSVParser
21
23
  from nautobot.core.api.utils import get_serializer_for_model
22
24
  from nautobot.core.api.views import ModelViewSet
23
25
  from nautobot.core.models.querysets import count_related
@@ -300,6 +302,7 @@ class DeviceTypeViewSet(NautobotModelViewSet):
300
302
  )
301
303
  serializer_class = serializers.DeviceTypeSerializer
302
304
  filterset_class = filters.DeviceTypeFilterSet
305
+ parser_classes = [JSONParser, NautobotCSVParser, MultiPartParser]
303
306
 
304
307
 
305
308
  #
@@ -903,6 +903,19 @@ class DeviceFilterSet(
903
903
  to_field_name="version",
904
904
  label="Software version (version or ID)",
905
905
  )
906
+ ip_addresses = MultiValueCharFilter(
907
+ method="filter_ip_addresses",
908
+ label="IP addresses (address or ID)",
909
+ distinct=True,
910
+ )
911
+ has_ip_addresses = RelatedMembershipBooleanFilter(field_name="interfaces__ip_addresses", label="Has IP addresses")
912
+
913
+ def filter_ip_addresses(self, queryset, name, value):
914
+ pk_values = set(item for item in value if is_uuid(item))
915
+ addresses = set(item for item in value if item not in pk_values)
916
+
917
+ ip_queryset = IPAddress.objects.filter_address_or_pk_in(addresses, pk_values)
918
+ return queryset.filter(interfaces__ip_addresses__in=ip_queryset).distinct()
906
919
 
907
920
  class Meta:
908
921
  model = Device
@@ -1130,6 +1143,19 @@ class InterfaceFilterSet(
1130
1143
  queryset=InterfaceRedundancyGroup.objects.all(),
1131
1144
  to_field_name="name",
1132
1145
  )
1146
+ ip_addresses = MultiValueCharFilter(
1147
+ method="filter_ip_addresses",
1148
+ label="IP addresses (address or ID)",
1149
+ distinct=True,
1150
+ )
1151
+ has_ip_addresses = RelatedMembershipBooleanFilter(field_name="ip_addresses", label="Has IP addresses")
1152
+
1153
+ def filter_ip_addresses(self, queryset, name, value):
1154
+ pk_values = set(item for item in value if is_uuid(item))
1155
+ addresses = set(item for item in value if item not in pk_values)
1156
+
1157
+ ip_queryset = IPAddress.objects.filter_address_or_pk_in(addresses, pk_values)
1158
+ return queryset.filter(ip_addresses__in=ip_queryset).distinct()
1133
1159
 
1134
1160
  class Meta:
1135
1161
  model = Interface
@@ -1144,7 +1170,6 @@ class InterfaceFilterSet(
1144
1170
  "description",
1145
1171
  "label",
1146
1172
  "tags",
1147
- "interface_redundancy_groups",
1148
1173
  ]
1149
1174
 
1150
1175
  def generate_query_filter_device(self, value):