nautobot 2.2.0b1__py3-none-any.whl → 2.2.2__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 (429) hide show
  1. nautobot/__init__.py +31 -0
  2. nautobot/apps/api.py +1 -2
  3. nautobot/apps/utils.py +4 -0
  4. nautobot/apps/views.py +2 -0
  5. nautobot/circuits/api/urls.py +1 -2
  6. nautobot/circuits/api/views.py +0 -12
  7. nautobot/circuits/apps.py +1 -1
  8. nautobot/circuits/tests/test_filters.py +1 -1
  9. nautobot/core/api/routers.py +50 -3
  10. nautobot/core/api/utils.py +4 -0
  11. nautobot/core/api/views.py +21 -15
  12. nautobot/core/cli/__init__.py +18 -11
  13. nautobot/core/constants.py +85 -0
  14. nautobot/core/filters.py +7 -1
  15. nautobot/core/forms/widgets.py +1 -2
  16. nautobot/core/graphql/schema.py +1 -0
  17. nautobot/core/management/commands/generate_test_data.py +4 -4
  18. nautobot/core/models/__init__.py +1 -0
  19. nautobot/core/settings.py +24 -3
  20. nautobot/core/settings.yaml +20 -0
  21. nautobot/core/signals.py +1 -0
  22. nautobot/core/tables.py +2 -1
  23. nautobot/core/templates/admin/base.html +23 -94
  24. nautobot/core/templates/generic/object_retrieve.html +2 -2
  25. nautobot/core/templates/graphene/graphiql.html +18 -47
  26. nautobot/core/templates/inc/footer.html +5 -5
  27. nautobot/core/templates/inc/javascript.html +4 -4
  28. nautobot/core/templates/inc/media.html +2 -2
  29. nautobot/core/templates/inc/nav_menu.html +0 -7
  30. nautobot/core/templates/nautobot_config.py.j2 +14 -1
  31. nautobot/core/templates/rest_framework/api.html +12 -5
  32. nautobot/core/templatetags/helpers.py +2 -2
  33. nautobot/core/testing/__init__.py +1 -1
  34. nautobot/core/testing/filters.py +1 -1
  35. nautobot/core/testing/views.py +30 -0
  36. nautobot/core/tests/integration/test_view_authentication.py +68 -0
  37. nautobot/core/tests/test_api.py +13 -6
  38. nautobot/core/tests/test_csv.py +5 -4
  39. nautobot/core/tests/test_filters.py +2 -1
  40. nautobot/core/tests/test_graphql.py +4 -14
  41. nautobot/core/tests/test_navigations.py +3 -0
  42. nautobot/core/tests/test_views.py +45 -16
  43. nautobot/core/utils/data.py +1 -2
  44. nautobot/core/utils/lookup.py +126 -0
  45. nautobot/core/views/__init__.py +3 -7
  46. nautobot/core/views/generic.py +24 -10
  47. nautobot/core/views/mixins.py +11 -4
  48. nautobot/core/views/renderers.py +11 -6
  49. nautobot/core/wsgi.py +9 -2
  50. nautobot/dcim/api/serializers.py +4 -4
  51. nautobot/dcim/api/urls.py +2 -3
  52. nautobot/dcim/api/views.py +7 -18
  53. nautobot/dcim/apps.py +8 -4
  54. nautobot/dcim/elevations.py +5 -1
  55. nautobot/dcim/factory.py +7 -7
  56. nautobot/dcim/filters/__init__.py +16 -17
  57. nautobot/dcim/forms.py +69 -48
  58. nautobot/dcim/homepage.py +11 -3
  59. nautobot/dcim/management/commands/migrate_location_contacts.py +218 -0
  60. nautobot/dcim/migrations/0057_controller_models.py +11 -70
  61. nautobot/dcim/models/__init__.py +2 -2
  62. nautobot/dcim/models/devices.py +14 -16
  63. nautobot/dcim/models/racks.py +1 -3
  64. nautobot/dcim/navigation.py +23 -31
  65. nautobot/dcim/signals.py +6 -6
  66. nautobot/dcim/tables/__init__.py +2 -2
  67. nautobot/dcim/tables/devices.py +13 -16
  68. nautobot/dcim/tables/template_code.py +1 -1
  69. nautobot/dcim/templates/dcim/controller_create.html +70 -0
  70. nautobot/dcim/templates/dcim/controller_retrieve.html +35 -18
  71. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +88 -0
  72. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +74 -42
  73. nautobot/dcim/templates/dcim/device.html +11 -3
  74. nautobot/dcim/templates/dcim/device_edit.html +1 -1
  75. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +4 -0
  76. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +1 -1
  77. nautobot/dcim/tests/test_api.py +47 -6
  78. nautobot/dcim/tests/test_filters.py +92 -81
  79. nautobot/dcim/tests/test_forms.py +49 -2
  80. nautobot/dcim/tests/test_graphql.py +11 -1
  81. nautobot/dcim/tests/test_models.py +15 -15
  82. nautobot/dcim/tests/test_signals.py +3 -1
  83. nautobot/dcim/tests/test_views.py +24 -12
  84. nautobot/dcim/urls.py +1 -1
  85. nautobot/dcim/views.py +25 -15
  86. nautobot/extras/api/serializers.py +20 -1
  87. nautobot/extras/api/urls.py +1 -2
  88. nautobot/extras/api/views.py +0 -10
  89. nautobot/extras/apps.py +7 -0
  90. nautobot/extras/context_managers.py +71 -4
  91. nautobot/extras/filters/__init__.py +53 -2
  92. nautobot/extras/filters/customfields.py +14 -9
  93. nautobot/extras/filters/mixins.py +6 -1
  94. nautobot/extras/forms/contacts.py +7 -0
  95. nautobot/extras/health_checks.py +1 -0
  96. nautobot/extras/jobs.py +1 -0
  97. nautobot/extras/managers.py +15 -2
  98. nautobot/extras/models/contacts.py +1 -0
  99. nautobot/extras/models/customfields.py +25 -2
  100. nautobot/extras/models/datasources.py +1 -0
  101. nautobot/extras/models/mixins.py +1 -0
  102. nautobot/extras/navigation.py +71 -65
  103. nautobot/extras/plugins/__init__.py +2 -1
  104. nautobot/extras/plugins/views.py +7 -11
  105. nautobot/extras/querysets.py +1 -2
  106. nautobot/extras/secrets/providers.py +1 -0
  107. nautobot/extras/signals.py +95 -51
  108. nautobot/extras/tasks.py +70 -17
  109. nautobot/extras/tests/test_api.py +2 -4
  110. nautobot/extras/tests/test_context_managers.py +98 -1
  111. nautobot/extras/tests/test_customfields.py +72 -9
  112. nautobot/extras/tests/test_dynamicgroups.py +2 -0
  113. nautobot/extras/tests/test_filters.py +89 -4
  114. nautobot/extras/tests/test_models.py +9 -0
  115. nautobot/extras/tests/test_relationships.py +10 -1
  116. nautobot/extras/tests/test_views.py +112 -1
  117. nautobot/extras/utils.py +37 -0
  118. nautobot/extras/views.py +18 -17
  119. nautobot/ipam/api/serializers.py +10 -0
  120. nautobot/ipam/api/urls.py +1 -2
  121. nautobot/ipam/api/views.py +0 -11
  122. nautobot/ipam/apps.py +3 -2
  123. nautobot/ipam/tables.py +3 -23
  124. nautobot/ipam/tests/test_graphql.py +2 -3
  125. nautobot/ipam/tests/test_tables.py +42 -0
  126. nautobot/ipam/tests/test_views.py +1 -0
  127. nautobot/ipam/views.py +9 -9
  128. nautobot/project-static/css/base.css +1 -0
  129. nautobot/project-static/docs/404.html +126 -73
  130. nautobot/project-static/docs/apps/index.html +127 -71
  131. nautobot/project-static/docs/apps/nautobot-apps.html +127 -71
  132. nautobot/project-static/docs/assets/javascripts/{bundle.8fd75fb4.min.js → bundle.bd41221c.min.js} +2 -2
  133. nautobot/project-static/docs/assets/javascripts/{bundle.8fd75fb4.min.js.map → bundle.bd41221c.min.js.map} +3 -3
  134. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +1 -0
  135. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +1 -0
  136. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +127 -71
  137. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +127 -71
  138. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +167 -73
  139. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +165 -72
  140. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +127 -71
  141. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +127 -71
  142. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +127 -71
  143. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +127 -71
  144. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +127 -71
  145. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +127 -71
  146. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +127 -71
  147. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +127 -71
  148. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +127 -71
  149. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +127 -71
  150. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +127 -71
  151. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +127 -71
  152. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +127 -71
  153. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +127 -71
  154. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +128 -72
  155. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +127 -71
  156. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +127 -71
  157. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +345 -71
  158. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +172 -73
  159. nautobot/project-static/docs/development/apps/api/configuration-view.html +127 -71
  160. nautobot/project-static/docs/development/apps/api/database-backend-config.html +127 -71
  161. nautobot/project-static/docs/development/apps/api/models/django-admin.html +127 -71
  162. nautobot/project-static/docs/development/apps/api/models/global-search.html +127 -71
  163. nautobot/project-static/docs/development/apps/api/models/graphql.html +127 -71
  164. nautobot/project-static/docs/development/apps/api/models/index.html +127 -71
  165. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +127 -71
  166. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +127 -71
  167. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +127 -71
  168. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +127 -71
  169. nautobot/project-static/docs/development/apps/api/platform-features/index.html +127 -71
  170. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +127 -71
  171. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +127 -71
  172. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +127 -71
  173. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +127 -71
  174. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +127 -71
  175. nautobot/project-static/docs/development/apps/api/prometheus.html +127 -71
  176. nautobot/project-static/docs/development/apps/api/setup.html +127 -71
  177. nautobot/project-static/docs/development/apps/api/testing.html +127 -71
  178. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +127 -71
  179. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +127 -71
  180. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +127 -71
  181. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +127 -71
  182. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +127 -71
  183. nautobot/project-static/docs/development/apps/api/views/base-template.html +127 -71
  184. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +141 -80
  185. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +144 -83
  186. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +127 -71
  187. nautobot/project-static/docs/development/apps/api/views/index.html +127 -71
  188. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +127 -71
  189. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +127 -71
  190. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +127 -71
  191. nautobot/project-static/docs/development/apps/api/views/notes.html +127 -71
  192. nautobot/project-static/docs/development/apps/api/views/rest-api.html +127 -71
  193. nautobot/project-static/docs/development/apps/api/views/urls.html +127 -71
  194. nautobot/project-static/docs/development/apps/index.html +127 -71
  195. nautobot/project-static/docs/development/apps/migration/code-updates.html +127 -71
  196. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +127 -71
  197. nautobot/project-static/docs/development/apps/migration/from-v1.html +127 -71
  198. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +127 -71
  199. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +127 -71
  200. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +127 -71
  201. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +127 -71
  202. nautobot/project-static/docs/development/apps/porting-from-netbox.html +127 -71
  203. nautobot/project-static/docs/development/core/application-registry.html +127 -71
  204. nautobot/project-static/docs/development/core/best-practices.html +145 -79
  205. nautobot/project-static/docs/development/core/bootstrap-ui.html +127 -71
  206. nautobot/project-static/docs/development/core/caching.html +127 -71
  207. nautobot/project-static/docs/development/core/controllers.html +141 -275
  208. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +127 -71
  209. nautobot/project-static/docs/development/core/extending-models.html +13 -8166
  210. nautobot/project-static/docs/development/core/generic-views.html +142 -86
  211. nautobot/project-static/docs/development/core/getting-started.html +146 -81
  212. nautobot/project-static/docs/development/core/homepage.html +145 -89
  213. nautobot/project-static/docs/development/core/index.html +127 -71
  214. nautobot/project-static/docs/development/core/model-checklist.html +8354 -0
  215. nautobot/project-static/docs/development/core/model-features.html +130 -74
  216. nautobot/project-static/docs/development/core/natural-keys.html +127 -71
  217. nautobot/project-static/docs/development/core/navigation-menu.html +127 -71
  218. nautobot/project-static/docs/development/core/release-checklist.html +127 -71
  219. nautobot/project-static/docs/development/core/role-internals.html +127 -71
  220. nautobot/project-static/docs/development/core/settings.html +127 -71
  221. nautobot/project-static/docs/development/core/style-guide.html +127 -71
  222. nautobot/project-static/docs/development/core/templates.html +127 -71
  223. nautobot/project-static/docs/development/core/testing.html +127 -71
  224. nautobot/project-static/docs/development/core/user-preferences.html +127 -71
  225. nautobot/project-static/docs/development/extending-models.html +3 -3
  226. nautobot/project-static/docs/development/index.html +127 -71
  227. nautobot/project-static/docs/development/jobs/index.html +128 -72
  228. nautobot/project-static/docs/development/jobs/migration/from-v1.html +127 -71
  229. nautobot/project-static/docs/index.html +126 -73
  230. nautobot/project-static/docs/models/dcim/{controllerdevicegroup.html → controllermanageddevicegroup.html} +3 -3
  231. nautobot/project-static/docs/objects.inv +0 -0
  232. nautobot/project-static/docs/release-notes/index.html +127 -71
  233. nautobot/project-static/docs/release-notes/version-1.0.html +127 -71
  234. nautobot/project-static/docs/release-notes/version-1.1.html +127 -71
  235. nautobot/project-static/docs/release-notes/version-1.2.html +127 -71
  236. nautobot/project-static/docs/release-notes/version-1.3.html +127 -71
  237. nautobot/project-static/docs/release-notes/version-1.4.html +127 -71
  238. nautobot/project-static/docs/release-notes/version-1.5.html +127 -71
  239. nautobot/project-static/docs/release-notes/version-1.6.html +663 -304
  240. nautobot/project-static/docs/release-notes/version-2.0.html +127 -71
  241. nautobot/project-static/docs/release-notes/version-2.1.html +538 -254
  242. nautobot/project-static/docs/release-notes/version-2.2.html +711 -125
  243. nautobot/project-static/docs/requirements.txt +3 -3
  244. nautobot/project-static/docs/search/search_index.json +1 -1
  245. nautobot/project-static/docs/sitemap.xml +264 -259
  246. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  247. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +127 -71
  248. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +127 -71
  249. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +127 -71
  250. nautobot/project-static/docs/user-guide/administration/configuration/index.html +127 -71
  251. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +192 -71
  252. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +127 -71
  253. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +127 -71
  254. nautobot/project-static/docs/user-guide/administration/guides/caching.html +127 -71
  255. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +127 -71
  256. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +127 -71
  257. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +127 -71
  258. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +131 -71
  259. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +127 -71
  260. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +127 -71
  261. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +130 -74
  262. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +127 -71
  263. nautobot/project-static/docs/user-guide/administration/installation/docker.html +134 -74
  264. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +127 -71
  265. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +8616 -0
  266. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +127 -71
  267. nautobot/project-static/docs/user-guide/administration/installation/index.html +127 -71
  268. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +127 -71
  269. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +127 -71
  270. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +130 -74
  271. nautobot/project-static/docs/user-guide/administration/installation/services.html +127 -71
  272. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +127 -71
  273. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +127 -71
  274. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +127 -71
  275. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +127 -71
  276. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +127 -71
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +127 -71
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +127 -71
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +127 -71
  280. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +127 -71
  281. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +127 -71
  282. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +127 -71
  283. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +127 -71
  284. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +127 -71
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +127 -71
  286. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +127 -71
  287. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +127 -71
  288. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +127 -71
  289. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +127 -71
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +127 -71
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +127 -71
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +127 -71
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +127 -71
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +127 -71
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +362 -79
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/{controllerdevicegroup.html → controllermanageddevicegroup.html} +210 -85
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +127 -71
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +127 -71
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +127 -71
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +127 -71
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +127 -71
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +127 -71
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +127 -71
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +127 -71
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +127 -71
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +127 -71
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +127 -71
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +127 -71
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +127 -71
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +127 -71
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +127 -71
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +127 -71
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +127 -71
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +127 -71
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +127 -71
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +127 -71
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +127 -71
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +127 -71
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +127 -71
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +127 -71
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +127 -71
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +127 -71
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +127 -71
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +127 -71
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +127 -71
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +127 -71
  327. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +130 -74
  328. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +127 -71
  329. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +138 -71
  330. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +138 -71
  331. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +127 -71
  332. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +127 -71
  333. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +127 -71
  334. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +127 -71
  335. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +127 -71
  336. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +127 -71
  337. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +127 -71
  338. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +127 -71
  339. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +127 -71
  340. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +127 -71
  341. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +127 -71
  342. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +127 -71
  343. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +127 -71
  344. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +127 -71
  345. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +127 -71
  346. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +127 -71
  347. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +127 -71
  348. nautobot/project-static/docs/user-guide/feature-guides/{contact-and-team.html → contacts-and-teams.html} +128 -72
  349. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +129 -73
  350. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +127 -71
  351. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +127 -71
  352. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +127 -71
  353. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +127 -71
  354. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +127 -71
  355. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +127 -71
  356. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +129 -73
  357. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +127 -71
  358. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +127 -71
  359. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +127 -71
  360. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +127 -71
  361. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +127 -71
  362. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +127 -71
  363. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +127 -71
  364. nautobot/project-static/docs/user-guide/index.html +127 -71
  365. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +127 -71
  366. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +127 -71
  367. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +127 -71
  368. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +127 -71
  369. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +127 -71
  370. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +127 -71
  371. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +127 -71
  372. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +127 -71
  373. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +127 -71
  374. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +127 -71
  375. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +127 -71
  376. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +127 -71
  377. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +127 -71
  378. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +127 -71
  379. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +127 -71
  380. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +127 -71
  381. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +127 -71
  382. nautobot/project-static/docs/user-guide/platform-functionality/note.html +127 -71
  383. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +127 -71
  384. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +127 -71
  385. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +127 -71
  386. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +127 -71
  387. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +127 -71
  388. nautobot/project-static/docs/user-guide/platform-functionality/role.html +127 -71
  389. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +127 -71
  390. nautobot/project-static/docs/user-guide/platform-functionality/status.html +127 -71
  391. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +127 -71
  392. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +127 -71
  393. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +127 -71
  394. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +127 -71
  395. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +127 -71
  396. nautobot/project-static/jquery/jquery-3.7.1.min.js +2 -0
  397. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_444444_256x240.png +0 -0
  398. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_555555_256x240.png +0 -0
  399. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_777620_256x240.png +0 -0
  400. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_777777_256x240.png +0 -0
  401. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_cc0000_256x240.png +0 -0
  402. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_ffffff_256x240.png +0 -0
  403. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +7 -0
  404. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +6 -0
  405. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +5 -0
  406. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/jquery-ui.theme.min.css +1 -1
  407. nautobot/tenancy/api/urls.py +1 -2
  408. nautobot/tenancy/api/views.py +0 -12
  409. nautobot/tenancy/tables.py +1 -1
  410. nautobot/tenancy/tests/test_views.py +1 -0
  411. nautobot/users/api/urls.py +1 -2
  412. nautobot/users/api/views.py +2 -65
  413. nautobot/users/views.py +8 -8
  414. nautobot/virtualization/api/urls.py +1 -2
  415. nautobot/virtualization/api/views.py +0 -12
  416. {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/METADATA +24 -24
  417. {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/RECORD +422 -416
  418. nautobot/dcim/templates/dcim/controllerdevicegroup_create.html +0 -43
  419. nautobot/project-static/docs/assets/stylesheets/main.f2e4d321.min.css +0 -1
  420. nautobot/project-static/docs/assets/stylesheets/main.f2e4d321.min.css.map +0 -1
  421. nautobot/project-static/jquery/jquery-3.6.0.min.js +0 -2
  422. nautobot/project-static/jquery-ui-1.13.1/jquery-ui.min.css +0 -7
  423. nautobot/project-static/jquery-ui-1.13.1/jquery-ui.min.js +0 -6
  424. nautobot/project-static/jquery-ui-1.13.1/jquery-ui.structure.min.css +0 -5
  425. /nautobot/dcim/templates/dcim/{controllerdevicegroup_retrieve.html → controllermanageddevicegroup_retrieve.html} +0 -0
  426. {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/LICENSE.txt +0 -0
  427. {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/NOTICE +0 -0
  428. {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/WHEEL +0 -0
  429. {nautobot-2.2.0b1.dist-info → nautobot-2.2.2.dist-info}/entry_points.txt +0 -0
nautobot/extras/tasks.py CHANGED
@@ -14,7 +14,7 @@ logger = getLogger("nautobot.extras.tasks")
14
14
 
15
15
 
16
16
  @nautobot_task
17
- def update_custom_field_choice_data(field_id, old_value, new_value):
17
+ def update_custom_field_choice_data(field_id, old_value, new_value, change_context=None):
18
18
  """
19
19
  Update the values for a custom field choice used in objects' _custom_field_data for the given field.
20
20
 
@@ -23,6 +23,8 @@ def update_custom_field_choice_data(field_id, old_value, new_value):
23
23
  old_value (str): The existing value of the choice
24
24
  new_value (str): The value which will be used as replacement
25
25
  """
26
+ # Circular Import
27
+ from nautobot.extras.context_managers import web_request_context
26
28
  from nautobot.extras.models import CustomField
27
29
 
28
30
  try:
@@ -35,19 +37,43 @@ def update_custom_field_choice_data(field_id, old_value, new_value):
35
37
  # Loop through all field content types and search for values to update
36
38
  for ct in field.content_types.all():
37
39
  model = ct.model_class()
38
- for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
39
- obj._custom_field_data[field.key] = new_value
40
- obj.save()
40
+ if change_context is not None:
41
+ with web_request_context(
42
+ user=change_context.get("user"),
43
+ change_id=change_context.get("change_id"),
44
+ context_detail=change_context.get("context_detail"),
45
+ context=change_context.get("context"),
46
+ ):
47
+ for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
48
+ obj._custom_field_data[field.key] = new_value
49
+ obj.save()
50
+ else:
51
+ for obj in model.objects.filter(**{f"_custom_field_data__{field.key}": old_value}):
52
+ obj._custom_field_data[field.key] = new_value
53
+ obj.save()
41
54
 
42
55
  elif field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
43
56
  # Loop through all field content types and search for values to update
44
57
  for ct in field.content_types.all():
45
58
  model = ct.model_class()
46
- for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
47
- old_list = obj._custom_field_data[field.key]
48
- new_list = [new_value if e == old_value else e for e in old_list]
49
- obj._custom_field_data[field.key] = new_list
50
- obj.save()
59
+ if change_context is not None:
60
+ with web_request_context(
61
+ user=change_context.get("user"),
62
+ change_id=change_context.get("change_id"),
63
+ context_detail=change_context.get("context_detail"),
64
+ context=change_context.get("context"),
65
+ ):
66
+ for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
67
+ old_list = obj._custom_field_data[field.key]
68
+ new_list = [new_value if e == old_value else e for e in old_list]
69
+ obj._custom_field_data[field.key] = new_list
70
+ obj.save()
71
+ else:
72
+ for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
73
+ old_list = obj._custom_field_data[field.key]
74
+ new_list = [new_value if e == old_value else e for e in old_list]
75
+ obj._custom_field_data[field.key] = new_list
76
+ obj.save()
51
77
 
52
78
  else:
53
79
  logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
@@ -57,7 +83,7 @@ def update_custom_field_choice_data(field_id, old_value, new_value):
57
83
 
58
84
 
59
85
  @nautobot_task
60
- def delete_custom_field_data(field_key, content_type_pk_set):
86
+ def delete_custom_field_data(field_key, content_type_pk_set, change_context=None):
61
87
  """
62
88
  Delete the values for a custom field
63
89
 
@@ -65,16 +91,30 @@ def delete_custom_field_data(field_key, content_type_pk_set):
65
91
  field_key (str): The key of the custom field which is being deleted
66
92
  content_type_pk_set (list): List of PKs for content types to act upon
67
93
  """
94
+ # Circular Import
95
+ from nautobot.extras.context_managers import web_request_context
96
+
68
97
  with transaction.atomic():
69
98
  for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
70
99
  model = ct.model_class()
71
- for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
72
- del obj._custom_field_data[field_key]
73
- obj.save()
100
+ if change_context is not None:
101
+ with web_request_context(
102
+ user=change_context.get("user"),
103
+ change_id=change_context.get("change_id"),
104
+ context_detail=change_context.get("context_detail"),
105
+ context=change_context.get("context"),
106
+ ):
107
+ for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
108
+ del obj._custom_field_data[field_key]
109
+ obj.save()
110
+ else:
111
+ for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
112
+ del obj._custom_field_data[field_key]
113
+ obj.save()
74
114
 
75
115
 
76
116
  @nautobot_task
77
- def provision_field(field_id, content_type_pk_set):
117
+ def provision_field(field_id, content_type_pk_set, change_context=None):
78
118
  """
79
119
  Provision a new custom field on all relevant content type object instances.
80
120
 
@@ -82,6 +122,8 @@ def provision_field(field_id, content_type_pk_set):
82
122
  field_id (uuid4): The PK of the custom field being provisioned
83
123
  content_type_pk_set (list): List of PKs for content types to act upon
84
124
  """
125
+ # Circular Import
126
+ from nautobot.extras.context_managers import web_request_context
85
127
  from nautobot.extras.models import CustomField
86
128
 
87
129
  try:
@@ -93,9 +135,20 @@ def provision_field(field_id, content_type_pk_set):
93
135
  with transaction.atomic():
94
136
  for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
95
137
  model = ct.model_class()
96
- for obj in model.objects.all():
97
- obj._custom_field_data.setdefault(field.key, field.default)
98
- obj.save()
138
+ if change_context is not None:
139
+ with web_request_context(
140
+ user=change_context.get("user"),
141
+ change_id=change_context.get("change_id"),
142
+ context_detail=change_context.get("context_detail"),
143
+ context=change_context.get("context"),
144
+ ):
145
+ for obj in model.objects.all():
146
+ obj._custom_field_data.setdefault(field.key, field.default)
147
+ obj.save()
148
+ else:
149
+ for obj in model.objects.all():
150
+ obj._custom_field_data.setdefault(field.key, field.default)
151
+ obj.save()
99
152
 
100
153
  return True
101
154
 
@@ -18,6 +18,7 @@ from nautobot.core.testing import APITestCase, APIViewTestCases
18
18
  from nautobot.core.testing.utils import disable_warnings
19
19
  from nautobot.core.utils.lookup import get_route_for_model
20
20
  from nautobot.dcim.models import (
21
+ Controller,
21
22
  Device,
22
23
  DeviceType,
23
24
  Location,
@@ -408,11 +409,9 @@ class ContactTest(APIViewTestCases.APIViewTestCase):
408
409
  {
409
410
  "name": "Contact 3",
410
411
  "phone": "555-0123",
411
- "email": "",
412
412
  },
413
413
  {
414
414
  "name": "Contact 4",
415
- "phone": "",
416
415
  "email": "contact4@example.com",
417
416
  },
418
417
  ]
@@ -2851,6 +2850,7 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
2851
2850
  vlan_groups = VLANGroup.objects.all()[:2]
2852
2851
 
2853
2852
  # Try deleting all devices and then creating 2 VLANs (fails):
2853
+ Controller.objects.filter(controller_device__isnull=False).delete()
2854
2854
  Device.objects.all().delete()
2855
2855
  response = send_bulk_data(
2856
2856
  "post",
@@ -3611,11 +3611,9 @@ class TeamTest(APIViewTestCases.APIViewTestCase):
3611
3611
  {
3612
3612
  "name": "Team 3",
3613
3613
  "phone": "555-0123",
3614
- "email": "",
3615
3614
  },
3616
3615
  {
3617
3616
  "name": "Team 4",
3618
- "phone": "",
3619
3617
  "email": "team4@example.com",
3620
3618
  "address": "Rainbow Bridge, Central NJ",
3621
3619
  },
@@ -7,7 +7,10 @@ from nautobot.core.testing import TransactionTestCase
7
7
  from nautobot.core.utils.lookup import get_changes_for_model
8
8
  from nautobot.dcim.models import Location, LocationType
9
9
  from nautobot.extras.choices import ObjectChangeActionChoices, ObjectChangeEventContextChoices
10
- from nautobot.extras.context_managers import web_request_context
10
+ from nautobot.extras.context_managers import (
11
+ deferred_change_logging_for_bulk_operation,
12
+ web_request_context,
13
+ )
11
14
  from nautobot.extras.models import Status, Webhook
12
15
 
13
16
  # Use the proper swappable User model
@@ -193,3 +196,97 @@ class WebRequestContextTransactionTestCase(TransactionTestCase):
193
196
  Status.objects.create(name="Test Status 2")
194
197
 
195
198
  self.assertEqual(get_changes_for_model(Status).count(), 2)
199
+
200
+
201
+ class BulkEditDeleteChangeLogging(TestCase):
202
+ def setUp(self):
203
+ self.user = User.objects.create_user(
204
+ username="jacob",
205
+ email="jacob@example.com",
206
+ password="top_secret", # noqa: S106 # hardcoded-password-func-arg -- ok as this is test code only
207
+ )
208
+
209
+ def test_change_log_created(self):
210
+ location_type = LocationType.objects.get(name="Campus")
211
+ location_status = Status.objects.get_for_model(Location).first()
212
+ with web_request_context(self.user):
213
+ with deferred_change_logging_for_bulk_operation():
214
+ location = Location(name="Test Location 1", location_type=location_type, status=location_status)
215
+ location.save()
216
+
217
+ location = Location.objects.get(name="Test Location 1")
218
+ oc_list = get_changes_for_model(location).order_by("pk")
219
+ self.assertEqual(len(oc_list), 1)
220
+ self.assertEqual(oc_list[0].changed_object, location)
221
+ self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
222
+
223
+ def test_delete(self):
224
+ """Test that deletes raise an exception"""
225
+ location_type = LocationType.objects.get(name="Campus")
226
+ location_status = Status.objects.get_for_model(Location).first()
227
+ with self.assertRaises(ValueError):
228
+ with web_request_context(self.user):
229
+ with deferred_change_logging_for_bulk_operation():
230
+ location = Location(name="Test Location 1", location_type=location_type, status=location_status)
231
+ location.save()
232
+ location.delete()
233
+
234
+ def test_create_then_update(self):
235
+ """Test that a create followed by an update is logged as a single create"""
236
+ location_type = LocationType.objects.get(name="Campus")
237
+ location_status = Status.objects.get_for_model(Location).first()
238
+ with web_request_context(self.user):
239
+ with deferred_change_logging_for_bulk_operation():
240
+ location = Location(name="Test Location 1", location_type=location_type, status=location_status)
241
+ location.save()
242
+ location.description = "changed"
243
+ location.save()
244
+
245
+ oc_list = get_changes_for_model(location)
246
+ self.assertEqual(len(oc_list), 1)
247
+ self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
248
+ snapshots = oc_list[0].get_snapshots()
249
+ self.assertIsNone(snapshots["prechange"])
250
+ self.assertIsNotNone(snapshots["postchange"])
251
+ self.assertIsNone(snapshots["differences"]["removed"])
252
+ self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
253
+
254
+ def test_bulk_edit(self):
255
+ """Test that edits to multiple objects are correctly logged"""
256
+ location_type = LocationType.objects.get(name="Campus")
257
+ location_status = Status.objects.get_for_model(Location).first()
258
+ locations = [
259
+ Location(name=f"Test Location {i}", location_type=location_type, status=location_status)
260
+ for i in range(1, 4)
261
+ ]
262
+ Location.objects.bulk_create(locations)
263
+ with web_request_context(self.user):
264
+ with deferred_change_logging_for_bulk_operation():
265
+ for location in locations:
266
+ location.description = "changed"
267
+ location.save()
268
+
269
+ oc_list = get_changes_for_model(Location)
270
+ self.assertEqual(len(oc_list), 3)
271
+ for oc in oc_list:
272
+ self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
273
+ snapshots = oc.get_snapshots()
274
+ self.assertIsNone(snapshots["prechange"])
275
+ self.assertIsNotNone(snapshots["postchange"])
276
+ self.assertIsNone(snapshots["differences"]["removed"])
277
+ self.assertEqual(snapshots["differences"]["added"]["description"], "changed")
278
+
279
+ def test_change_log_context(self):
280
+ location_type = LocationType.objects.get(name="Campus")
281
+ location_status = Status.objects.get_for_model(Location).first()
282
+ with web_request_context(self.user, context_detail="test_change_log_context"):
283
+ with deferred_change_logging_for_bulk_operation():
284
+ location = Location(name="Test Location 1", location_type=location_type, status=location_status)
285
+ location.save()
286
+
287
+ location = Location.objects.get(name="Test Location 1")
288
+ oc_list = get_changes_for_model(location)
289
+ with self.subTest():
290
+ self.assertEqual(oc_list[0].change_context, ObjectChangeEventContextChoices.CONTEXT_ORM)
291
+ with self.subTest():
292
+ self.assertEqual(oc_list[0].change_context_detail, "test_change_log_context")
@@ -16,11 +16,13 @@ from nautobot.core.tables import CustomFieldColumn
16
16
  from nautobot.core.testing import APITestCase, TestCase, TransactionTestCase
17
17
  from nautobot.core.testing.models import ModelTestCases
18
18
  from nautobot.core.testing.utils import post_data
19
+ from nautobot.core.utils.lookup import get_changes_for_model
19
20
  from nautobot.dcim.filters import LocationFilterSet
20
21
  from nautobot.dcim.forms import RackFilterForm
21
22
  from nautobot.dcim.models import Device, Location, LocationType, Rack
22
23
  from nautobot.dcim.tables import LocationTable
23
24
  from nautobot.extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
25
+ from nautobot.extras.context_managers import web_request_context
24
26
  from nautobot.extras.models import ComputedField, CustomField, CustomFieldChoice, Status
25
27
  from nautobot.users.models import ObjectPermission
26
28
  from nautobot.virtualization.models import VirtualMachine
@@ -1537,8 +1539,10 @@ class CustomFieldFilterTest(TestCase):
1537
1539
  cf.save()
1538
1540
  cf.content_types.set([obj_type])
1539
1541
 
1540
- CustomFieldChoice.objects.create(custom_field=cf, value="Foo")
1541
- CustomFieldChoice.objects.create(custom_field=cf, value="Bar")
1542
+ cls.select_choices = (
1543
+ CustomFieldChoice.objects.create(custom_field=cf, value="Foo"),
1544
+ CustomFieldChoice.objects.create(custom_field=cf, value="Bar"),
1545
+ )
1542
1546
 
1543
1547
  # Multi-select filtering
1544
1548
  cf = CustomField(
@@ -1548,8 +1552,11 @@ class CustomFieldFilterTest(TestCase):
1548
1552
  cf.save()
1549
1553
  cf.content_types.set([obj_type])
1550
1554
 
1551
- CustomFieldChoice.objects.create(custom_field=cf, value="Foo")
1552
- CustomFieldChoice.objects.create(custom_field=cf, value="Bar")
1555
+ cls.multiselect_choices = (
1556
+ CustomFieldChoice.objects.create(custom_field=cf, value="Foo"),
1557
+ CustomFieldChoice.objects.create(custom_field=cf, value="Bar"),
1558
+ )
1559
+
1553
1560
  cls.location_type = LocationType.objects.get(name="Campus")
1554
1561
  location_status = Status.objects.get_for_model(Location).first()
1555
1562
  Location.objects.create(
@@ -1844,6 +1851,10 @@ class CustomFieldFilterTest(TestCase):
1844
1851
  self.filterset({"cf_cf8": ["Foo", "AR"]}, self.queryset).qs,
1845
1852
  self.queryset.filter(_custom_field_data__cf8__in=["Foo", "AR"]),
1846
1853
  )
1854
+ self.assertQuerysetEqualAndNotEmpty( # https://github.com/nautobot/nautobot/issues/5009
1855
+ self.filterset({"cf_cf8": [str(choice.pk) for choice in self.select_choices]}, self.queryset).qs,
1856
+ self.queryset.filter(_custom_field_data__cf8__in=[choice.value for choice in self.select_choices]),
1857
+ )
1847
1858
  self.assertQuerysetEqual(
1848
1859
  self.filterset({"cf_cf8__n": ["Foo"]}, self.queryset).qs,
1849
1860
  self.queryset.exclude(_custom_field_data__cf8="Foo")
@@ -1913,6 +1924,10 @@ class CustomFieldFilterTest(TestCase):
1913
1924
  self.filterset({"cf_cf9": "Bar"}, self.queryset).qs,
1914
1925
  self.queryset.filter(_custom_field_data__cf9__contains="Bar"),
1915
1926
  )
1927
+ self.assertQuerysetEqualAndNotEmpty( # https://github.com/nautobot/nautobot/issues/5009
1928
+ self.filterset({"cf_cf9": str(self.multiselect_choices[0].pk)}, self.queryset).qs,
1929
+ self.queryset.filter(_custom_field_data__cf9__contains=self.multiselect_choices[0].value),
1930
+ )
1916
1931
 
1917
1932
 
1918
1933
  class CustomFieldChoiceTest(ModelTestCases.BaseModelTestCase):
@@ -2016,30 +2031,64 @@ class CustomFieldBackgroundTasks(TransactionTestCase):
2016
2031
 
2017
2032
  self.assertEqual(location.cf["cf1"], "Foo")
2018
2033
 
2034
+ with web_request_context(self.user):
2035
+ cf = CustomField(label="CF2", type=CustomFieldTypeChoices.TYPE_TEXT, default="Bar")
2036
+ cf.save()
2037
+ cf.content_types.set([obj_type])
2038
+
2039
+ location.refresh_from_db()
2040
+
2041
+ self.assertEqual(location.cf["cf2"], "Bar")
2042
+
2043
+ oc_list = get_changes_for_model(location).order_by("pk")
2044
+ self.assertEqual(len(oc_list), 1)
2045
+ self.assertEqual(oc_list[0].changed_object, location)
2046
+ self.assertEqual(oc_list[0].change_context_detail, "provision custom field data for new content types")
2047
+ self.assertEqual(oc_list[0].user, self.user)
2048
+
2019
2049
  def test_delete_custom_field_data_task(self):
2020
2050
  obj_type = ContentType.objects.get_for_model(Location)
2021
- cf = CustomField(
2051
+ cf_1 = CustomField(
2022
2052
  label="CF1",
2023
2053
  type=CustomFieldTypeChoices.TYPE_TEXT,
2024
2054
  )
2025
- cf.save()
2026
- cf.content_types.set([obj_type])
2055
+ cf_1.save()
2056
+ cf_1.content_types.set([obj_type])
2057
+ cf_2 = CustomField(
2058
+ label="CF2",
2059
+ type=CustomFieldTypeChoices.TYPE_TEXT,
2060
+ )
2061
+ cf_2.save()
2062
+ cf_2.content_types.set([obj_type])
2027
2063
  location_type = LocationType.objects.create(name="Root Type 2")
2028
2064
  location_status = Status.objects.get_for_model(Location).first()
2029
2065
  location = Location(
2030
2066
  name="Location 1",
2031
2067
  location_type=location_type,
2032
2068
  status=location_status,
2033
- _custom_field_data={"cf1": "foo"},
2069
+ _custom_field_data={"cf1": "foo", "cf2": "bar"},
2034
2070
  )
2035
2071
  location.save()
2036
2072
 
2037
- cf.delete()
2073
+ cf_1.delete()
2038
2074
 
2039
2075
  location.refresh_from_db()
2040
2076
 
2041
2077
  self.assertTrue("cf1" not in location.cf)
2042
2078
 
2079
+ with web_request_context(self.user):
2080
+ cf_2.delete()
2081
+
2082
+ location.refresh_from_db()
2083
+
2084
+ self.assertTrue("cf2" not in location.cf)
2085
+
2086
+ oc_list = get_changes_for_model(location).order_by("pk")
2087
+ self.assertEqual(len(oc_list), 1)
2088
+ self.assertEqual(oc_list[0].changed_object, location)
2089
+ self.assertEqual(oc_list[0].change_context_detail, "delete custom field data")
2090
+ self.assertEqual(oc_list[0].user, self.user)
2091
+
2043
2092
  def test_update_custom_field_choice_data_task(self):
2044
2093
  obj_type = ContentType.objects.get_for_model(Location)
2045
2094
  cf = CustomField(
@@ -2068,6 +2117,20 @@ class CustomFieldBackgroundTasks(TransactionTestCase):
2068
2117
 
2069
2118
  self.assertEqual(location.cf["cf1"], "Bar")
2070
2119
 
2120
+ with web_request_context(self.user):
2121
+ choice.value = "FizzBuzz"
2122
+ choice.save()
2123
+
2124
+ location.refresh_from_db()
2125
+
2126
+ self.assertEqual(location.cf["cf1"], "FizzBuzz")
2127
+
2128
+ oc_list = get_changes_for_model(location).order_by("pk")
2129
+ self.assertEqual(len(oc_list), 1)
2130
+ self.assertEqual(oc_list[0].changed_object, location)
2131
+ self.assertEqual(oc_list[0].change_context_detail, "update custom field choice data")
2132
+ self.assertEqual(oc_list[0].user, self.user)
2133
+
2071
2134
 
2072
2135
  class CustomFieldTableTest(TestCase):
2073
2136
  """
@@ -16,6 +16,7 @@ from nautobot.dcim.choices import PortTypeChoices
16
16
  from nautobot.dcim.filters import DeviceFilterSet
17
17
  from nautobot.dcim.forms import DeviceFilterForm, DeviceForm
18
18
  from nautobot.dcim.models import (
19
+ Controller,
19
20
  Device,
20
21
  DeviceType,
21
22
  FrontPort,
@@ -48,6 +49,7 @@ from nautobot.tenancy.models import Tenant
48
49
  class DynamicGroupTestBase(TestCase):
49
50
  @classmethod
50
51
  def setUpTestData(cls):
52
+ Controller.objects.filter(controller_device__isnull=False).delete()
51
53
  Device.objects.all().delete()
52
54
  cls.device_ct = ContentType.objects.get_for_model(Device)
53
55
  cls.dynamicgroup_ct = ContentType.objects.get_for_model(DynamicGroup)
@@ -61,6 +61,7 @@ from nautobot.extras.models import (
61
61
  ComputedField,
62
62
  ConfigContext,
63
63
  Contact,
64
+ ContactAssociation,
64
65
  CustomField,
65
66
  CustomFieldChoice,
66
67
  CustomLink,
@@ -457,7 +458,91 @@ class ContentTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
457
458
  )
458
459
 
459
460
 
460
- class ContactFilterSetTestCase(FilterTestCases.FilterTestCase):
461
+ class ContactAndTeamFilterSetTestCaseMixin:
462
+ """Mixin class to test common filters to both Contact and Team filter sets."""
463
+
464
+ def test_similar_to_location_data(self):
465
+ """Complex test to test the complex `similar_to_location_data` method filter."""
466
+ ContactAssociation.objects.all().delete()
467
+ self.queryset.delete()
468
+ location_type = LocationType.objects.filter(parent__isnull=True).first()
469
+ location_status = Status.objects.get_for_model(Location).first()
470
+ test_locations = (
471
+ Location.objects.create(
472
+ location_type=location_type,
473
+ name="Filter Test Location 0",
474
+ status=location_status,
475
+ contact_name="match 0",
476
+ ),
477
+ Location.objects.create(
478
+ location_type=location_type,
479
+ name="Filter Test Location 1",
480
+ status=location_status,
481
+ contact_email="Test email for location 1 and 2",
482
+ ),
483
+ Location.objects.create(
484
+ location_type=location_type,
485
+ name="Filter Test Location 2",
486
+ status=location_status,
487
+ contact_email="TEST EMAIL FOR LOCATION 1 AND 2",
488
+ contact_phone="Test phone for location 2 and 3",
489
+ ),
490
+ Location.objects.create(
491
+ location_type=location_type,
492
+ name="Filter Test Location 3",
493
+ status=location_status,
494
+ contact_phone="Test phone for location 2 and 3",
495
+ ),
496
+ Location.objects.create(
497
+ location_type=location_type,
498
+ name="Filter Test Location 4",
499
+ status=location_status,
500
+ contact_name="Hopefully this doesn't match any random factory data",
501
+ contact_email="Hopefully this doesn't match any random factory data",
502
+ contact_phone="Hopefully this doesn't match any random factory data",
503
+ physical_address="Hopefully this doesn't match any random factory data",
504
+ shipping_address="Hopefully this doesn't match any random factory data",
505
+ ),
506
+ )
507
+
508
+ self.queryset.create(name="match 0")
509
+ self.queryset.create(name="match 1 and 2", email="Test email for location 1 and 2")
510
+ self.queryset.create(name="match 2 and 3", phone="Test phone for location 2 and 3")
511
+
512
+ # These subtests are confusing because we're trying to test the NaturalKeyOrPKMultipleChoiceFilter
513
+ # behavior while also testing the `similar_to_location_data` method filter behavior.
514
+ with self.subTest("Test name match"):
515
+ params = {"similar_to_location_data": [test_locations[0].pk]}
516
+ self.assertQuerysetEqualAndNotEmpty(
517
+ self.filterset(params, self.queryset).qs,
518
+ self.queryset.filter(name__in=["match 0"]),
519
+ )
520
+ with self.subTest("Test email match"):
521
+ params = {"similar_to_location_data": [test_locations[1].pk]}
522
+ self.assertQuerysetEqualAndNotEmpty(
523
+ self.filterset(params, self.queryset).qs,
524
+ self.queryset.filter(name__in=["match 1 and 2"]),
525
+ )
526
+ with self.subTest("Test phone match"):
527
+ params = {"similar_to_location_data": [test_locations[2].pk]}
528
+ self.assertQuerysetEqualAndNotEmpty(
529
+ self.filterset(params, self.queryset).qs,
530
+ self.queryset.filter(name__in=["match 1 and 2", "match 2 and 3"]),
531
+ )
532
+ with self.subTest("Test email and phone match"):
533
+ params = {"similar_to_location_data": [test_locations[1].pk, test_locations[3].name]}
534
+ self.assertQuerysetEqualAndNotEmpty(
535
+ self.filterset(params, self.queryset).qs,
536
+ self.queryset.filter(name__in=["match 1 and 2", "match 2 and 3"]),
537
+ )
538
+ with self.subTest("Test no match"):
539
+ params = {"similar_to_location_data": [test_locations[4].pk]}
540
+ self.assertFalse(self.filterset(params, self.queryset).qs.exists())
541
+ params = {"similar_to_location_data": [test_locations[4].name]}
542
+ self.assertFalse(self.filterset(params, self.queryset).qs.exists())
543
+
544
+
545
+ class ContactFilterSetTestCase(ContactAndTeamFilterSetTestCaseMixin, FilterTestCases.FilterTestCase):
461
546
  queryset = Contact.objects.all()
462
547
  filterset = ContactFilterSet
463
548
 
@@ -1756,7 +1841,7 @@ class TagTestCase(FilterTestCases.NameOnlyFilterTestCase):
1756
1841
  self.assertEqual(self.filterset(params, self.queryset).qs.values_list("pk", flat=True)[0], value)
1757
1842
 
1758
1843
 
1759
- class TeamFilterSetTestCase(FilterTestCases.FilterTestCase):
1844
+ class TeamFilterSetTestCase(ContactAndTeamFilterSetTestCaseMixin, FilterTestCases.FilterTestCase):
1760
1845
  queryset = Team.objects.all()
1761
1846
  filterset = TeamFilterSet
1762
1847
 
@@ -1838,8 +1923,8 @@ class RoleTestCase(FilterTestCases.NameOnlyFilterTestCase):
1838
1923
  def test_content_types(self):
1839
1924
  device_ct = ContentType.objects.get_for_model(Device)
1840
1925
  rack_ct = ContentType.objects.get_for_model(Rack)
1841
- device_roles = self.queryset.filter(content_types=device_ct)
1842
- params = {"content_types": ["dcim.device"]}
1926
+ device_roles = self.queryset.filter(content_types__in=[device_ct, rack_ct]).distinct()
1927
+ params = {"content_types": ["dcim.device", "dcim.rack"]}
1843
1928
  self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, device_roles)
1844
1929
 
1845
1930
  rack_roles = self.queryset.filter(content_types=rack_ct)
@@ -1810,6 +1810,15 @@ class JobResultTestCase(TestCase):
1810
1810
  JobResult.objects.create(name="ExampleJob2", user=None, result=lambda: 1)
1811
1811
  self.assertEqual(str(err.exception), "Object of type function is not JSON serializable")
1812
1812
 
1813
+ def test_get_task(self):
1814
+ """Assert bug fix for `Cannot resolve keyword 'task_id' into field` #5440"""
1815
+ data = {
1816
+ "output": "valid data",
1817
+ }
1818
+ job_result = JobResult.objects.create(name="ExampleJob1", user=None, result=data)
1819
+
1820
+ self.assertEqual(JobResult.objects.get_task(job_result.pk), job_result)
1821
+
1813
1822
 
1814
1823
  class WebhookTest(ModelTestCases.BaseModelTestCase):
1815
1824
  model = Webhook
@@ -15,7 +15,15 @@ from nautobot.core.tables import RelationshipColumn
15
15
  from nautobot.core.testing import TestCase
16
16
  from nautobot.core.testing.models import ModelTestCases
17
17
  from nautobot.core.utils.lookup import get_route_for_model
18
- from nautobot.dcim.models import Device, DeviceTypeToSoftwareImageFile, Location, LocationType, Platform, Rack
18
+ from nautobot.dcim.models import (
19
+ Controller,
20
+ Device,
21
+ DeviceTypeToSoftwareImageFile,
22
+ Location,
23
+ LocationType,
24
+ Platform,
25
+ Rack,
26
+ )
19
27
  from nautobot.dcim.tables import LocationTable
20
28
  from nautobot.dcim.tests.test_views import create_test_device
21
29
  from nautobot.extras.choices import RelationshipRequiredSideChoices, RelationshipSideChoices, RelationshipTypeChoices
@@ -1165,6 +1173,7 @@ class RequiredRelationshipTestMixin:
1165
1173
  # Protected FK to SoftwareImageFile prevents deletion
1166
1174
  DeviceTypeToSoftwareImageFile.objects.all().delete()
1167
1175
  # Protected FK to SoftwareVersion prevents deletion
1176
+ Controller.objects.all().delete()
1168
1177
  Device.objects.all().update(software_version=None)
1169
1178
 
1170
1179
  # Create required relationships: