nautobot 2.4.6__py3-none-any.whl → 2.4.7__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 (347) hide show
  1. nautobot/apps/forms.py +2 -0
  2. nautobot/circuits/templates/circuits/providernetwork.html +1 -1
  3. nautobot/circuits/templates/circuits/providernetwork_retrieve.html +2 -55
  4. nautobot/circuits/views.py +20 -23
  5. nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +2 -40
  6. nautobot/cloud/views.py +12 -0
  7. nautobot/core/api/urls.py +2 -2
  8. nautobot/core/api/views.py +39 -1
  9. nautobot/core/forms/__init__.py +2 -0
  10. nautobot/core/forms/fields.py +2 -2
  11. nautobot/core/forms/widgets.py +18 -0
  12. nautobot/core/templates/widgets/sluginput.html +5 -1
  13. nautobot/core/templatetags/helpers.py +15 -1
  14. nautobot/core/testing/integration.py +6 -2
  15. nautobot/core/ui/object_detail.py +16 -3
  16. nautobot/core/utils/lookup.py +2 -2
  17. nautobot/dcim/forms.py +10 -0
  18. nautobot/dcim/models/locations.py +9 -0
  19. nautobot/dcim/templates/dcim/device_list.html +1 -1
  20. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  21. nautobot/dcim/templates/dcim/manufacturer.html +1 -63
  22. nautobot/dcim/templates/dcim/module_list.html +1 -1
  23. nautobot/dcim/templates/dcim/moduletype_retrieve.html +1 -1
  24. nautobot/dcim/tests/integration/test_module_bay_position.py +125 -0
  25. nautobot/dcim/tests/test_models.py +13 -0
  26. nautobot/dcim/tests/test_views.py +4 -1
  27. nautobot/dcim/urls.py +1 -45
  28. nautobot/dcim/views.py +35 -66
  29. nautobot/extras/choices.py +4 -0
  30. nautobot/extras/filters/__init__.py +6 -0
  31. nautobot/extras/forms/forms.py +76 -8
  32. nautobot/extras/models/customfields.py +10 -9
  33. nautobot/extras/signals.py +43 -4
  34. nautobot/extras/tasks.py +4 -2
  35. nautobot/extras/templates/extras/contact_retrieve.html +1 -58
  36. nautobot/extras/templates/extras/exporttemplate.html +1 -53
  37. nautobot/extras/templates/extras/team_retrieve.html +1 -58
  38. nautobot/extras/tests/test_customfields.py +24 -0
  39. nautobot/extras/tests/test_views.py +22 -0
  40. nautobot/extras/urls.py +2 -70
  41. nautobot/extras/views.py +101 -79
  42. nautobot/ipam/tables.py +1 -0
  43. nautobot/project-static/css/base.css +5 -0
  44. nautobot/project-static/docs/404.html +0 -2
  45. nautobot/project-static/docs/apps/index.html +0 -2
  46. nautobot/project-static/docs/apps/nautobot-apps.html +0 -2
  47. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +0 -2
  48. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +0 -2
  49. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +0 -2
  50. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +0 -2
  51. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +0 -2
  52. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +0 -2
  53. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +0 -2
  54. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +0 -2
  55. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +0 -2
  56. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +0 -2
  57. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +0 -2
  58. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +0 -2
  59. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +62 -2
  60. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +0 -2
  61. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +0 -2
  62. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +0 -2
  63. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +0 -2
  64. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +0 -2
  65. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +0 -2
  66. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +0 -2
  67. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +7 -5
  68. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +0 -2
  69. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +0 -2
  70. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +0 -2
  71. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -2
  72. nautobot/project-static/docs/development/apps/api/database-backend-config.html +0 -2
  73. nautobot/project-static/docs/development/apps/api/models/django-admin.html +0 -2
  74. nautobot/project-static/docs/development/apps/api/models/global-search.html +0 -2
  75. nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -2
  76. nautobot/project-static/docs/development/apps/api/models/index.html +0 -2
  77. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +0 -2
  78. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +0 -2
  79. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -2
  80. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +0 -2
  81. nautobot/project-static/docs/development/apps/api/platform-features/index.html +0 -2
  82. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -2
  83. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +0 -2
  84. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -2
  85. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -2
  86. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +0 -2
  87. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +0 -2
  88. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -2
  89. nautobot/project-static/docs/development/apps/api/setup.html +0 -2
  90. nautobot/project-static/docs/development/apps/api/testing.html +0 -2
  91. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -2
  92. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -2
  93. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +0 -2
  94. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +0 -2
  95. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -2
  96. nautobot/project-static/docs/development/apps/api/views/base-template.html +0 -2
  97. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -2
  98. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +0 -2
  99. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +0 -2
  100. nautobot/project-static/docs/development/apps/api/views/index.html +0 -2
  101. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +0 -2
  102. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -2
  103. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -2
  104. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -2
  105. nautobot/project-static/docs/development/apps/api/views/rest-api.html +0 -2
  106. nautobot/project-static/docs/development/apps/api/views/urls.html +0 -2
  107. nautobot/project-static/docs/development/apps/index.html +0 -2
  108. nautobot/project-static/docs/development/apps/migration/code-updates.html +0 -2
  109. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +0 -2
  110. nautobot/project-static/docs/development/apps/migration/from-v1.html +0 -2
  111. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +0 -2
  112. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +0 -2
  113. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +0 -2
  114. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +0 -2
  115. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +0 -2
  116. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +0 -2
  117. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +0 -2
  118. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +0 -2
  119. nautobot/project-static/docs/development/apps/porting-from-netbox.html +0 -2
  120. nautobot/project-static/docs/development/core/application-registry.html +0 -2
  121. nautobot/project-static/docs/development/core/best-practices.html +0 -2
  122. nautobot/project-static/docs/development/core/bootstrap-ui.html +0 -2
  123. nautobot/project-static/docs/development/core/caching.html +0 -2
  124. nautobot/project-static/docs/development/core/controllers.html +0 -2
  125. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +0 -2
  126. nautobot/project-static/docs/development/core/generic-views.html +0 -2
  127. nautobot/project-static/docs/development/core/getting-started.html +0 -2
  128. nautobot/project-static/docs/development/core/homepage.html +0 -2
  129. nautobot/project-static/docs/development/core/index.html +0 -2
  130. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +0 -2
  131. nautobot/project-static/docs/development/core/model-checklist.html +0 -2
  132. nautobot/project-static/docs/development/core/model-features.html +0 -2
  133. nautobot/project-static/docs/development/core/natural-keys.html +0 -2
  134. nautobot/project-static/docs/development/core/navigation-menu.html +0 -2
  135. nautobot/project-static/docs/development/core/release-checklist.html +0 -2
  136. nautobot/project-static/docs/development/core/role-internals.html +0 -2
  137. nautobot/project-static/docs/development/core/settings.html +0 -2
  138. nautobot/project-static/docs/development/core/style-guide.html +0 -2
  139. nautobot/project-static/docs/development/core/templates.html +0 -2
  140. nautobot/project-static/docs/development/core/testing.html +0 -2
  141. nautobot/project-static/docs/development/core/ui-component-framework.html +0 -2
  142. nautobot/project-static/docs/development/core/user-preferences.html +0 -2
  143. nautobot/project-static/docs/development/index.html +0 -2
  144. nautobot/project-static/docs/development/jobs/index.html +0 -2
  145. nautobot/project-static/docs/development/jobs/migration/from-v1.html +0 -2
  146. nautobot/project-static/docs/index.html +0 -2
  147. nautobot/project-static/docs/objects.inv +0 -0
  148. nautobot/project-static/docs/overview/application_stack.html +0 -2
  149. nautobot/project-static/docs/overview/design_philosophy.html +0 -2
  150. nautobot/project-static/docs/release-notes/index.html +0 -2
  151. nautobot/project-static/docs/release-notes/version-1.0.html +0 -2
  152. nautobot/project-static/docs/release-notes/version-1.1.html +0 -2
  153. nautobot/project-static/docs/release-notes/version-1.2.html +0 -2
  154. nautobot/project-static/docs/release-notes/version-1.3.html +0 -2
  155. nautobot/project-static/docs/release-notes/version-1.4.html +0 -2
  156. nautobot/project-static/docs/release-notes/version-1.5.html +0 -2
  157. nautobot/project-static/docs/release-notes/version-1.6.html +0 -2
  158. nautobot/project-static/docs/release-notes/version-2.0.html +0 -2
  159. nautobot/project-static/docs/release-notes/version-2.1.html +0 -2
  160. nautobot/project-static/docs/release-notes/version-2.2.html +0 -2
  161. nautobot/project-static/docs/release-notes/version-2.3.html +0 -2
  162. nautobot/project-static/docs/release-notes/version-2.4.html +138 -2
  163. nautobot/project-static/docs/search/search_index.json +1 -1
  164. nautobot/project-static/docs/sitemap.xml +290 -290
  165. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  166. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +0 -2
  167. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +0 -2
  168. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +0 -2
  169. nautobot/project-static/docs/user-guide/administration/configuration/index.html +0 -2
  170. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +0 -2
  171. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -2
  172. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +0 -2
  173. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +0 -2
  174. nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -2
  175. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +0 -2
  176. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +0 -2
  177. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +0 -2
  178. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +0 -2
  179. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +0 -2
  180. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +0 -2
  181. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +0 -2
  182. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +0 -2
  183. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +0 -2
  184. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +0 -2
  185. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -2
  186. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +0 -2
  187. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +0 -2
  188. nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -2
  189. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +0 -2
  190. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +0 -2
  191. nautobot/project-static/docs/user-guide/administration/security/index.html +0 -2
  192. nautobot/project-static/docs/user-guide/administration/security/notices.html +0 -2
  193. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +0 -2
  194. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +0 -2
  195. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +0 -2
  196. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +0 -2
  197. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +0 -2
  198. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +0 -2
  199. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +0 -2
  200. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +0 -2
  201. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +0 -2
  202. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +0 -2
  203. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -2
  204. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +0 -2
  205. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +0 -2
  206. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +0 -2
  207. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +0 -2
  208. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -2
  209. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +0 -2
  210. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +0 -2
  211. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +0 -2
  212. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +0 -2
  213. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +0 -2
  214. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +0 -2
  215. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +0 -2
  216. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +0 -2
  217. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -2
  218. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -2
  219. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -2
  220. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -2
  221. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +0 -2
  222. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +0 -2
  223. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +0 -2
  224. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -2
  225. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -2
  226. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +0 -2
  227. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -2
  228. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -2
  229. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -2
  230. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -2
  231. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +0 -2
  232. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -2
  233. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -2
  234. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -2
  235. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -2
  236. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +0 -2
  237. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +0 -2
  238. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +0 -2
  239. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +0 -2
  240. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +0 -2
  241. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +0 -2
  242. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -2
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +0 -2
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -2
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -2
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +0 -2
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -2
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -2
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +0 -2
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +0 -2
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +0 -2
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -2
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -2
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +0 -2
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +0 -2
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +0 -2
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +0 -2
  258. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -2
  259. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -2
  260. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +0 -2
  261. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +0 -2
  262. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -2
  263. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +0 -2
  264. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +0 -2
  265. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +0 -2
  266. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +0 -2
  267. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +0 -2
  268. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -2
  269. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +0 -2
  270. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +0 -2
  271. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +0 -2
  272. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +0 -2
  273. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +0 -2
  274. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +0 -2
  275. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +0 -2
  276. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +0 -2
  277. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +0 -2
  278. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -2
  279. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +0 -2
  280. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +0 -2
  281. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +0 -2
  282. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +0 -2
  283. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +0 -2
  284. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +0 -2
  285. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +0 -2
  286. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +0 -2
  287. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +0 -2
  288. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +0 -2
  289. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +0 -2
  290. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +0 -2
  291. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +0 -2
  292. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +0 -2
  293. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -2
  294. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +0 -2
  295. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -2
  296. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +0 -2
  297. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +0 -2
  298. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +0 -2
  299. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +0 -2
  300. nautobot/project-static/docs/user-guide/index.html +0 -2
  301. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +0 -2
  302. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -2
  303. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +0 -2
  304. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +0 -2
  305. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -2
  306. nautobot/project-static/docs/user-guide/platform-functionality/events.html +0 -2
  307. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +0 -2
  308. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +0 -2
  309. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -2
  310. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -2
  311. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +0 -2
  312. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +0 -2
  313. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -2
  314. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -2
  315. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -2
  316. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -2
  317. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +0 -2
  318. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +0 -2
  319. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -2
  320. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +0 -2
  321. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -2
  322. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +0 -2
  323. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +0 -2
  324. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +0 -2
  325. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -2
  326. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -2
  327. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -2
  328. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +0 -2
  329. nautobot/project-static/docs/user-guide/platform-functionality/role.html +0 -2
  330. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +0 -2
  331. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +0 -2
  332. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +0 -2
  333. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -2
  334. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -2
  335. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +0 -2
  336. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +0 -2
  337. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +0 -2
  338. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -2
  339. nautobot/project-static/js/forms.js +88 -37
  340. nautobot/project-static/js/homepage_layout.js +12 -3
  341. {nautobot-2.4.6.dist-info → nautobot-2.4.7.dist-info}/METADATA +1 -1
  342. {nautobot-2.4.6.dist-info → nautobot-2.4.7.dist-info}/RECORD +346 -346
  343. nautobot/dcim/templates/dcim/modulebay_create.html +0 -39
  344. {nautobot-2.4.6.dist-info → nautobot-2.4.7.dist-info}/LICENSE.txt +0 -0
  345. {nautobot-2.4.6.dist-info → nautobot-2.4.7.dist-info}/NOTICE +0 -0
  346. {nautobot-2.4.6.dist-info → nautobot-2.4.7.dist-info}/WHEEL +0 -0
  347. {nautobot-2.4.6.dist-info → nautobot-2.4.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,125 @@
1
+ from django.urls import reverse
2
+
3
+ from nautobot.core.testing.integration import ObjectsListMixin, SeleniumTestCase
4
+ from nautobot.dcim.models import DeviceType, Manufacturer, Module, ModuleBay, ModuleType
5
+ from nautobot.extras.models import Status
6
+ from nautobot.extras.tests.integration import create_test_device
7
+
8
+
9
+ class ModuleBayPositionTestCase(SeleniumTestCase, ObjectsListMixin):
10
+ """
11
+ Test creating a module bay component in device and device type.
12
+ """
13
+
14
+ def _validate_position_field(self):
15
+ # Fill name pattern
16
+ name_pattern_field = self.browser.find_by_css("#id_name_pattern")
17
+ name_pattern_value = "name-0/0/[0-9]"
18
+ for _ in name_pattern_field.type(name_pattern_value, slowly=True):
19
+ pass
20
+
21
+ # Verify that position is filled
22
+ position_field = self.browser.find_by_css("#id_position_pattern")
23
+ self.assertEqual(position_field.value, name_pattern_value, "Position field value is not properly set")
24
+
25
+ # Change pattern manually and name to verify if it's not updating then
26
+ position_field.fill("")
27
+ for _ in position_field.type("new pattern", slowly=True):
28
+ pass
29
+
30
+ for _ in name_pattern_field.type("v2", slowly=True):
31
+ pass
32
+
33
+ self.assertEqual(position_field.value, "new pattern", "Position field value has unexpectedly changed")
34
+
35
+ # Regenerate position
36
+ self.browser.find_by_css('button[data-original-title="Regenerate position"]').click()
37
+ self.assertEqual(position_field.value, f"{name_pattern_value}v2", "Position field value is not re-populated")
38
+
39
+ def test_create_device_type_module_bay(self):
40
+ self.login_as_superuser()
41
+
42
+ manufacturer, _ = Manufacturer.objects.get_or_create(
43
+ name="Test Manufacturer",
44
+ )
45
+ device_type, _ = DeviceType.objects.get_or_create(manufacturer=manufacturer, model="Test Model Module Bay")
46
+
47
+ details_url = self.live_server_url + reverse("dcim:devicetype", kwargs={"pk": device_type.pk})
48
+ self.browser.visit(details_url)
49
+
50
+ # Navigate to module bay create page
51
+ self.browser.find_by_css("#device-type-add-components-button").click()
52
+ self.browser.find_by_xpath(
53
+ "//*[@id='device-type-add-components-button']/following-sibling::*//a[normalize-space()='Module Bays']"
54
+ ).click()
55
+
56
+ self._validate_position_field()
57
+
58
+ def test_create_device_module_bay(self):
59
+ self.login_as_superuser()
60
+
61
+ device = create_test_device("Test Device Module Bay Integration Test 1")
62
+ details_url = self.live_server_url + reverse("dcim:device", kwargs={"pk": device.pk})
63
+ self.browser.visit(details_url)
64
+
65
+ # Navigate to module bay create page
66
+ self.browser.find_by_css("#device-add-components-button").click()
67
+ self.browser.find_by_xpath(
68
+ "//*[@id='device-add-components-button']/following-sibling::*//a[normalize-space()='Module Bays']"
69
+ ).click()
70
+
71
+ self._validate_position_field()
72
+
73
+ def test_bulk_create_device_module_bay(self):
74
+ self.login_as_superuser()
75
+
76
+ device = create_test_device("Test Device Module Bay Integration Test 1", test_uuid="a15a58b0b")
77
+ self.browser.visit(self.live_server_url + reverse("dcim:device_list"))
78
+
79
+ self.apply_filter("location", "a15a58b0b")
80
+
81
+ self.select_one_item(pk=device.pk)
82
+ self.browser.find_by_css("#device-bulk-add-components-button").click()
83
+ self.browser.find_by_xpath(
84
+ "//*[@id='device-bulk-add-components-button']/following-sibling::*//a[normalize-space()='Module Bays']"
85
+ ).click()
86
+
87
+ self._validate_position_field()
88
+
89
+ def test_create_module_type_module_bay(self):
90
+ self.login_as_superuser()
91
+
92
+ manufacturer, _ = Manufacturer.objects.get_or_create(name="Test Manufacturer")
93
+ module_type = ModuleType.objects.create(model="Module_Type", manufacturer=manufacturer)
94
+
95
+ details_url = self.live_server_url + reverse("dcim:moduletype", kwargs={"pk": module_type.pk})
96
+ self.browser.visit(details_url)
97
+
98
+ self.browser.find_by_css("#module-type-add-components-button").click()
99
+ self.browser.find_by_xpath(
100
+ "//*[@id='module-type-add-components-button']/following-sibling::*//a[normalize-space()='Module Bays']"
101
+ ).click()
102
+
103
+ self._validate_position_field()
104
+
105
+ def test_bulk_create_module_module_bay(self):
106
+ self.login_as_superuser()
107
+
108
+ device = create_test_device("Test Device Module Bay Integration Test 2", test_uuid="60a7d5e")
109
+ module_type = ModuleType.objects.create(model="Module_Type", manufacturer=device.device_type.manufacturer)
110
+ device_module_bay = ModuleBay.objects.create(parent_device=device, name="Test Bay")
111
+ module = Module.objects.create(
112
+ module_type=module_type,
113
+ status=Status.objects.get_for_model(Module).first(),
114
+ parent_module_bay=device_module_bay,
115
+ )
116
+
117
+ self.browser.visit(self.live_server_url + reverse("dcim:module_list"))
118
+ self.select_one_item(pk=module.pk)
119
+
120
+ self.browser.find_by_css("#module-bulk-add-components-button").click()
121
+ self.browser.find_by_xpath(
122
+ "//*[@id='module-bulk-add-components-button']/following-sibling::*//a[normalize-space()='Module Bays']"
123
+ ).click()
124
+
125
+ self._validate_position_field()
@@ -1338,6 +1338,19 @@ class LocationTestCase(ModelTestCases.BaseModelTestCase):
1338
1338
  str(cm.exception),
1339
1339
  )
1340
1340
 
1341
+ def test_default_treemodel_display(self):
1342
+ location_1 = Location(name="Building 1", location_type=self.root_type, status=self.status)
1343
+ location_1.validated_save()
1344
+ location_2 = Location(name="Room 1", location_type=self.leaf_type, parent=location_1, status=self.status)
1345
+ self.assertEqual(location_2.display, "Building 1 → Room 1")
1346
+
1347
+ @override_settings(LOCATION_NAME_AS_NATURAL_KEY=True)
1348
+ def test_location_name_as_natural_key_display(self):
1349
+ location_1 = Location(name="Building 1", location_type=self.root_type, status=self.status)
1350
+ location_1.validated_save()
1351
+ location_2 = Location(name="Room 1", location_type=self.leaf_type, parent=location_1, status=self.status)
1352
+ self.assertEqual(location_2.display, "Room 1")
1353
+
1341
1354
 
1342
1355
  class PlatformTestCase(TestCase):
1343
1356
  def setUp(self):
@@ -760,7 +760,7 @@ class DeviceFamilyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
760
760
  DeviceFamily.objects.create(name="Deletable Device Family 3")
761
761
 
762
762
 
763
- class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
763
+ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase, ViewTestCases.BulkEditObjectsViewTestCase):
764
764
  model = Manufacturer
765
765
 
766
766
  @classmethod
@@ -769,6 +769,9 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
769
769
  "name": "Manufacturer X",
770
770
  "description": "A new manufacturer",
771
771
  }
772
+ cls.bulk_edit_data = {
773
+ "description": "Updated manufacturer description",
774
+ }
772
775
 
773
776
  def get_deletable_object(self):
774
777
  mf = Manufacturer.objects.create(name="Deletable Manufacturer")
nautobot/dcim/urls.py CHANGED
@@ -17,7 +17,6 @@ from .models import (
17
17
  Interface,
18
18
  InventoryItem,
19
19
  Location,
20
- Manufacturer,
21
20
  Platform,
22
21
  PowerFeed,
23
22
  PowerOutlet,
@@ -40,6 +39,7 @@ router.register("device-redundancy-groups", views.DeviceRedundancyGroupUIViewSet
40
39
  router.register("interface-redundancy-groups", views.InterfaceRedundancyGroupUIViewSet)
41
40
  router.register("interface-redundancy-groups-associations", views.InterfaceRedundancyGroupAssociationUIViewSet)
42
41
  router.register("location-types", views.LocationTypeUIViewSet)
42
+ router.register("manufacturers", views.ManufacturerUIViewSet)
43
43
  router.register("module-bays", views.ModuleBayUIViewSet)
44
44
  router.register("module-bay-templates", views.ModuleBayTemplateUIViewSet)
45
45
  router.register("modules", views.ModuleUIViewSet)
@@ -208,50 +208,6 @@ urlpatterns = [
208
208
  name="rack_add_image",
209
209
  kwargs={"model": Rack},
210
210
  ),
211
- # Manufacturers
212
- path("manufacturers/", views.ManufacturerListView.as_view(), name="manufacturer_list"),
213
- path(
214
- "manufacturers/add/",
215
- views.ManufacturerEditView.as_view(),
216
- name="manufacturer_add",
217
- ),
218
- path(
219
- "manufacturers/import/",
220
- views.ManufacturerBulkImportView.as_view(), # 3.0 TODO: remove, unused
221
- name="manufacturer_import",
222
- ),
223
- path(
224
- "manufacturers/delete/",
225
- views.ManufacturerBulkDeleteView.as_view(),
226
- name="manufacturer_bulk_delete",
227
- ),
228
- path(
229
- "manufacturers/<uuid:pk>/",
230
- views.ManufacturerView.as_view(),
231
- name="manufacturer",
232
- ),
233
- path(
234
- "manufacturers/<uuid:pk>/edit/",
235
- views.ManufacturerEditView.as_view(),
236
- name="manufacturer_edit",
237
- ),
238
- path(
239
- "manufacturers/<uuid:pk>/delete/",
240
- views.ManufacturerDeleteView.as_view(),
241
- name="manufacturer_delete",
242
- ),
243
- path(
244
- "manufacturers/<uuid:pk>/changelog/",
245
- ObjectChangeLogView.as_view(),
246
- name="manufacturer_changelog",
247
- kwargs={"model": Manufacturer},
248
- ),
249
- path(
250
- "manufacturers/<uuid:pk>/notes/",
251
- ObjectNotesView.as_view(),
252
- name="manufacturer_notes",
253
- kwargs={"model": Manufacturer},
254
- ),
255
211
  # Device types
256
212
  path("device-types/", views.DeviceTypeListView.as_view(), name="devicetype_list"),
257
213
  path("device-types/add/", views.DeviceTypeEditView.as_view(), name="devicetype_add"),
nautobot/dcim/views.py CHANGED
@@ -28,7 +28,6 @@ from rest_framework.exceptions import MethodNotAllowed
28
28
  from rest_framework.response import Response
29
29
 
30
30
  from nautobot.circuits.models import Circuit
31
- from nautobot.cloud.models import CloudAccount
32
31
  from nautobot.cloud.tables import CloudAccountTable
33
32
  from nautobot.core.choices import ButtonColorChoices
34
33
  from nautobot.core.exceptions import AbortTransaction
@@ -304,7 +303,7 @@ class LocationView(generic.ObjectView):
304
303
  .select_related("parent", "location_type")
305
304
  )
306
305
 
307
- children_table = tables.LocationTable(children)
306
+ children_table = tables.LocationTable(children, hide_hierarchy_ui=True)
308
307
 
309
308
  paginate = {
310
309
  "paginator_class": EnhancedPaginator,
@@ -724,71 +723,40 @@ class RackReservationBulkDeleteView(generic.BulkDeleteView):
724
723
  #
725
724
 
726
725
 
727
- class ManufacturerListView(generic.ObjectListView):
726
+ class ManufacturerUIViewSet(NautobotUIViewSet):
727
+ bulk_update_form_class = forms.ManufacturerBulkEditForm
728
+ filterset_class = filters.ManufacturerFilterSet
729
+ filterset_form_class = forms.ManufacturerFilterForm
730
+ form_class = forms.ManufacturerForm
731
+ serializer_class = serializers.ManufacturerSerializer
732
+ table_class = tables.ManufacturerTable
728
733
  queryset = Manufacturer.objects.all()
729
- filterset = filters.ManufacturerFilterSet
730
- filterset_form = forms.ManufacturerFilterForm
731
- table = tables.ManufacturerTable
732
734
 
733
-
734
- class ManufacturerView(generic.ObjectView):
735
- queryset = Manufacturer.objects.all()
736
-
737
- def get_extra_context(self, request, instance):
738
- # Devices
739
- devices = (
740
- Device.objects.restrict(request.user, "view")
741
- .filter(device_type__manufacturer=instance)
742
- .select_related("status", "location", "tenant", "role", "rack", "device_type")
743
- )
744
-
745
- device_table = tables.DeviceTable(devices)
746
-
747
- paginate = {
748
- "paginator_class": EnhancedPaginator,
749
- "per_page": get_paginate_count(request),
750
- }
751
- RequestConfig(request, paginate).configure(device_table)
752
-
753
- # Cloud Accounts
754
- cloud_accounts = (
755
- CloudAccount.objects.restrict(request.user, "view")
756
- .filter(provider=instance)
757
- .select_related("secrets_group")
758
- )
759
-
760
- cloud_account_table = CloudAccountTable(cloud_accounts)
761
- paginate = {
762
- "paginator_class": EnhancedPaginator,
763
- "per_page": get_paginate_count(request),
764
- }
765
- RequestConfig(request, paginate).configure(cloud_account_table)
766
-
767
- return {
768
- "device_table": device_table,
769
- "cloud_account_table": cloud_account_table,
770
- **super().get_extra_context(request, instance),
771
- }
772
-
773
-
774
- class ManufacturerEditView(generic.ObjectEditView):
775
- queryset = Manufacturer.objects.all()
776
- model_form = forms.ManufacturerForm
777
-
778
-
779
- class ManufacturerDeleteView(generic.ObjectDeleteView):
780
- queryset = Manufacturer.objects.all()
781
-
782
-
783
- class ManufacturerBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
784
- queryset = Manufacturer.objects.all()
785
- table = tables.ManufacturerTable
786
-
787
-
788
- class ManufacturerBulkDeleteView(generic.BulkDeleteView):
789
- queryset = Manufacturer.objects.all()
790
- table = tables.ManufacturerTable
791
- filterset = filters.ManufacturerFilterSet
735
+ # Object detail content with devices and cloud accounts related to the manufacturer
736
+ object_detail_content = object_detail.ObjectDetailContent(
737
+ panels=(
738
+ object_detail.ObjectFieldsPanel(
739
+ weight=100,
740
+ section=SectionChoices.LEFT_HALF,
741
+ fields="__all__",
742
+ ),
743
+ object_detail.ObjectsTablePanel(
744
+ weight=100,
745
+ section=SectionChoices.FULL_WIDTH,
746
+ table_class=tables.DeviceTable,
747
+ table_filter="device_type__manufacturer",
748
+ related_field_name="manufacturer",
749
+ exclude_columns=["manufacturer"],
750
+ ),
751
+ object_detail.ObjectsTablePanel(
752
+ weight=100,
753
+ section=SectionChoices.FULL_WIDTH,
754
+ table_class=CloudAccountTable,
755
+ table_filter="provider",
756
+ exclude_columns=["provider"],
757
+ ),
758
+ ),
759
+ )
792
760
 
793
761
 
794
762
  #
@@ -1769,6 +1737,7 @@ class DeviceView(generic.ObjectView):
1769
1737
  weight=100,
1770
1738
  color=ButtonColorChoices.BLUE,
1771
1739
  label="Add Components",
1740
+ attributes={"id": "device-add-components-button"},
1772
1741
  icon="mdi-plus-thick",
1773
1742
  required_permissions=["dcim.change_device"],
1774
1743
  children=(
@@ -3436,7 +3405,7 @@ class ModuleBayUIViewSet(ModuleBayCommonViewSetMixin, NautobotUIViewSet):
3436
3405
  model_form_class = forms.ModuleBayForm
3437
3406
  serializer_class = serializers.ModuleBaySerializer
3438
3407
  table_class = tables.ModuleBayTable
3439
- create_template_name = "dcim/modulebay_create.html"
3408
+ create_template_name = "dcim/device_component_add.html"
3440
3409
 
3441
3410
  def get_extra_context(self, request, instance):
3442
3411
  if instance:
@@ -488,6 +488,8 @@ class SecretsGroupSecretTypeChoices(ChoiceSet):
488
488
  TYPE_SECRET = "secret" # noqa: S105 # hardcoded-password-string -- false positive
489
489
  TYPE_TOKEN = "token" # noqa: S105 # hardcoded-password-string -- false positive
490
490
  TYPE_USERNAME = "username"
491
+ TYPE_URL = "url"
492
+ TYPE_NOTES = "notes"
491
493
 
492
494
  CHOICES = (
493
495
  (TYPE_KEY, "Key"),
@@ -495,6 +497,8 @@ class SecretsGroupSecretTypeChoices(ChoiceSet):
495
497
  (TYPE_SECRET, "Secret"),
496
498
  (TYPE_TOKEN, "Token"),
497
499
  (TYPE_USERNAME, "Username"),
500
+ (TYPE_URL, "URL"),
501
+ (TYPE_NOTES, "Notes"),
498
502
  )
499
503
 
500
504
 
@@ -526,6 +526,12 @@ class ContactTeamFilterSet(NameSearchFilterSet, NautobotFilterSet):
526
526
 
527
527
 
528
528
  class ContactFilterSet(ContactTeamFilterSet):
529
+ teams = NaturalKeyOrPKMultipleChoiceFilter(
530
+ queryset=Team.objects.all(),
531
+ to_field_name="name",
532
+ label="Team (name or ID)",
533
+ )
534
+
529
535
  class Meta:
530
536
  model = Contact
531
537
  fields = "__all__"
@@ -142,6 +142,7 @@ __all__ = (
142
142
  "DynamicGroupFilterForm",
143
143
  "DynamicGroupForm",
144
144
  "DynamicGroupMembershipFormSet",
145
+ "ExportTemplateBulkEditForm",
145
146
  "ExportTemplateFilterForm",
146
147
  "ExportTemplateForm",
147
148
  "ExternalIntegrationBulkEditForm",
@@ -180,6 +181,7 @@ __all__ = (
180
181
  "ObjectMetadataFilterForm",
181
182
  "PasswordInputWithPlaceholder",
182
183
  "RelationshipAssociationFilterForm",
184
+ "RelationshipBulkEditForm",
183
185
  "RelationshipFilterForm",
184
186
  "RelationshipForm",
185
187
  "RoleBulkEditForm",
@@ -519,10 +521,11 @@ class CustomFieldBulkDeleteForm(ConfirmationForm):
519
521
  else:
520
522
  context = change_context.as_dict(queryset)
521
523
  context["context_detail"] = "bulk delete custom field data"
522
- tasks = [
523
- delete_custom_field_data.si(obj.key, set(obj.content_types.values_list("pk", flat=True)), context)
524
- for obj in queryset
525
- ]
524
+ tasks = []
525
+ for obj in queryset:
526
+ pk_set = set(obj.content_types.values_list("pk", flat=True))
527
+ if pk_set:
528
+ tasks.append(delete_custom_field_data.si(obj.key, pk_set, context))
526
529
  return tasks
527
530
 
528
531
  def perform_pre_delete(self, queryset):
@@ -533,10 +536,11 @@ class CustomFieldBulkDeleteForm(ConfirmationForm):
533
536
  logger.error("Celery worker process not running. Object custom fields may fail to reflect this deletion.")
534
537
  return
535
538
  tasks = self.construct_custom_field_delete_tasks(queryset)
536
- # Executing the tasks in the background sequentially using chain() aligns with how a single
537
- # CustomField object is deleted. We decided to not check the result because it needs at least one worker
538
- # to be active and comes with extra performance penalty.
539
- chain(*tasks).apply_async()
539
+ if tasks:
540
+ # Executing the tasks in the background sequentially using chain() aligns with how a single
541
+ # CustomField object is deleted. We decided to not check the result because it needs at least one worker
542
+ # to be active and comes with extra performance penalty.
543
+ chain(*tasks).apply_async()
540
544
 
541
545
 
542
546
  #
@@ -749,6 +753,31 @@ class StaticGroupAssociationFilterForm(NautobotFilterForm):
749
753
  #
750
754
  # Export Templates
751
755
  #
756
+ class ExportTemplateBulkEditForm(NautobotBulkEditForm):
757
+ pk = forms.ModelMultipleChoiceField(queryset=ExportTemplate.objects.all(), widget=forms.MultipleHiddenInput())
758
+
759
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
760
+ mime_type = forms.CharField(
761
+ max_length=CHARFIELD_MAX_LENGTH,
762
+ required=False,
763
+ label="MIME type",
764
+ help_text="Defaults to <code>text/plain</code>",
765
+ )
766
+ file_extension = forms.CharField(
767
+ max_length=CHARFIELD_MAX_LENGTH, required=False, help_text="Extension to append to the rendered filename"
768
+ )
769
+
770
+ content_type = forms.ModelChoiceField(
771
+ queryset=ContentType.objects.filter(FeatureQuery("export_templates").get_query()).order_by(
772
+ "app_label", "model"
773
+ ),
774
+ required=False,
775
+ label="Content Type",
776
+ )
777
+
778
+ class Meta:
779
+ model = ExportTemplate
780
+ nullable_fields = ["description", "mime_type", "file_extension"]
752
781
 
753
782
 
754
783
  class ExportTemplateForm(BootstrapMixin, forms.ModelForm):
@@ -1774,6 +1803,45 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
1774
1803
  #
1775
1804
 
1776
1805
 
1806
+ class RelationshipBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditFormMixin, NoteModelBulkEditFormMixin):
1807
+ pk = forms.ModelMultipleChoiceField(queryset=Relationship.objects.all(), widget=forms.MultipleHiddenInput())
1808
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1809
+ type = forms.ChoiceField(
1810
+ required=False,
1811
+ label="type",
1812
+ choices=add_blank_choice(RelationshipTypeChoices),
1813
+ )
1814
+ source_hidden = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
1815
+ destination_hidden = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
1816
+ source_filter = forms.JSONField(required=False, widget=forms.Textarea, help_text="Filter for the source")
1817
+ destination_filter = forms.JSONField(required=False, widget=forms.Textarea, help_text="Filter for the destination")
1818
+ source_type = CSVContentTypeField(
1819
+ queryset=ContentType.objects.filter(FeatureQuery("relationships").get_query()), required=False
1820
+ )
1821
+ destination_type = CSVContentTypeField(
1822
+ queryset=ContentType.objects.filter(FeatureQuery("relationships").get_query()), required=False
1823
+ )
1824
+ source_label = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1825
+ destination_label = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
1826
+ advanced_ui = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
1827
+
1828
+ class Meta:
1829
+ model = Relationship
1830
+ fields = [
1831
+ "description",
1832
+ "type",
1833
+ "source_hidden",
1834
+ "destination_hidden",
1835
+ "source_filter",
1836
+ "destination_filter",
1837
+ "source_type",
1838
+ "destination_type",
1839
+ "source_label",
1840
+ "destination_label",
1841
+ "advanced_ui",
1842
+ ]
1843
+
1844
+
1777
1845
  class RelationshipForm(BootstrapMixin, forms.ModelForm):
1778
1846
  key = SlugField(
1779
1847
  help_text="Internal name of this relationship. Please use underscores rather than dashes.",
@@ -822,16 +822,17 @@ class CustomField(
822
822
 
823
823
  super().delete(*args, **kwargs)
824
824
 
825
- # Circular Import
826
- from nautobot.extras.signals import change_context_state
825
+ if content_types:
826
+ # Circular Import
827
+ from nautobot.extras.signals import change_context_state
827
828
 
828
- change_context = change_context_state.get()
829
- if change_context is None:
830
- context = None
831
- else:
832
- context = change_context.as_dict(instance=self)
833
- context["context_detail"] = "delete custom field data"
834
- delete_custom_field_data.delay(self.key, content_types, context)
829
+ change_context = change_context_state.get()
830
+ if change_context is None:
831
+ context = None
832
+ else:
833
+ context = change_context.as_dict(instance=self)
834
+ context["context_detail"] = "delete custom field data"
835
+ delete_custom_field_data.delay(self.key, content_types, context)
835
836
 
836
837
  def add_prefix_to_cf_key(self):
837
838
  return "cf_" + str(self.key)
@@ -125,6 +125,17 @@ def invalidate_relationship_models_cache(sender, **kwargs):
125
125
  cache.delete_pattern(f"{method.cache_key_prefix}.*")
126
126
 
127
127
 
128
+ @receiver(post_save, sender=CustomField)
129
+ @receiver(post_delete, sender=CustomField)
130
+ @receiver(post_save, sender=Relationship)
131
+ @receiver(m2m_changed, sender=Relationship)
132
+ @receiver(post_delete, sender=Relationship)
133
+ def invalidate_openapi_schema_cache(sender, **kwargs):
134
+ """Invalidate the openapi schema cache."""
135
+ with contextlib.suppress(redis.exceptions.ConnectionError):
136
+ cache.delete_pattern("openapi_schema_cache_*")
137
+
138
+
128
139
  @receiver(post_save)
129
140
  @receiver(m2m_changed)
130
141
  def _handle_changed_object(sender, instance, raw=False, **kwargs):
@@ -327,7 +338,9 @@ def post_migrate_clear_content_type_caches(sender, app_config, signal, **kwargs)
327
338
 
328
339
  def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
329
340
  """
330
- Handle the cleanup of old custom field data when a CustomField is removed from one or more ContentTypes.
341
+ Handle provisioning/deprovisioning of custom_field_data when there are changes to CustomField.content_types.
342
+
343
+ The name of this function is misleading as this signal applies to *added* content-types as well.
331
344
  """
332
345
 
333
346
  change_context = change_context_state.get()
@@ -335,13 +348,39 @@ def handle_cf_removed_obj_types(instance, action, pk_set, **kwargs):
335
348
  context = None
336
349
  else:
337
350
  context = change_context.as_dict(instance=instance)
338
- if action == "post_remove":
339
- # Existing content types have been removed from the custom field, delete their data
351
+
352
+ if action == "pre_remove":
353
+ # Existing content types may be removed from the custom field, delete their data if so.
354
+ # CAUTION: pk_set in this _remove case is the content-types that were *requested* to remove,
355
+ # **not** the content-types that actually *will need to be* removed. In other words, this is not idempotent:
356
+ # my_cf.content_types.remove(device_ct) --> pk_set = {device_ct.pk}
357
+ # my_cf.content_types.remove(device_ct) --> pk_set = {device_ct.pk} again even though it was already gone
358
+ # So we need to check which content types will actually be removed to not create unnecessary tasks:
359
+ removed_pk_set = pk_set.intersection(instance.content_types.values_list("pk", flat=True))
360
+ if not removed_pk_set:
361
+ return
362
+
363
+ if context:
364
+ context["context_detail"] = "delete custom field data from existing content types"
365
+ transaction.on_commit(lambda: delete_custom_field_data.delay(instance.key, removed_pk_set, context))
366
+
367
+ elif action == "pre_clear":
368
+ # In this case, the provided pk_set is always empty, so we need to look at the current values instead:
369
+ cleared_pk_set = set(instance.content_types.values_list("pk", flat=True))
370
+ if not cleared_pk_set:
371
+ return
372
+
340
373
  if context:
341
374
  context["context_detail"] = "delete custom field data from existing content types"
342
- transaction.on_commit(lambda: delete_custom_field_data.delay(instance.key, pk_set, context))
375
+ transaction.on_commit(lambda: delete_custom_field_data.delay(instance.key, cleared_pk_set, context))
343
376
 
344
377
  elif action == "post_add":
378
+ # Unlike the above _remove case, in the _add case pk_set is the *new* content-types only,
379
+ # and for whatever reason, Django triggers this signal even if there was no actual change.
380
+ # To avoid creating unnecessary background tasks, we need to check for this case ourselves:
381
+ if not pk_set:
382
+ return
383
+
345
384
  # New content types have been added to the custom field, provision them
346
385
  if context:
347
386
  context["context_detail"] = "provision custom field data for new content types"
nautobot/extras/tasks.py CHANGED
@@ -121,12 +121,13 @@ def delete_custom_field_data(field_key, content_type_pk_set, change_context=None
121
121
  for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
122
122
  model = ct.model_class()
123
123
  queryset = model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False})
124
+ pk_list = []
124
125
  if change_context is not None:
125
126
  pk_list = list(queryset.values_list("pk", flat=True))
126
127
  task_logger.info("Deleting existing values for custom field `%s` from %s records...", field_key, ct.model)
127
128
  count = queryset.update(_custom_field_data=JSONRemove("_custom_field_data", field_key))
128
129
  task_logger.info("Updated %d records", count)
129
- if change_context is not None:
130
+ if count and change_context is not None:
130
131
  # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
131
132
  _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
132
133
 
@@ -155,6 +156,7 @@ def provision_field(field_id, content_type_pk_set, change_context=None):
155
156
  for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
156
157
  model = ct.model_class()
157
158
  queryset = model.objects.filter(**{f"_custom_field_data__{field.key}__isnull": True})
159
+ pk_list = []
158
160
  if change_context is not None:
159
161
  pk_list = list(queryset.values_list("pk", flat=True))
160
162
  task_logger.info(
@@ -165,7 +167,7 @@ def provision_field(field_id, content_type_pk_set, change_context=None):
165
167
  )
166
168
  count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, field.default))
167
169
  task_logger.info("Updated %d records.", count)
168
- if change_context is not None:
170
+ if count and change_context is not None:
169
171
  # Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
170
172
  _generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
171
173