nautobot 2.2.0b1__py3-none-any.whl → 2.2.1__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 (425) 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 +44 -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 +20 -6
  47. nautobot/core/views/mixins.py +7 -1
  48. nautobot/core/views/renderers.py +11 -6
  49. nautobot/dcim/api/serializers.py +4 -4
  50. nautobot/dcim/api/urls.py +2 -3
  51. nautobot/dcim/api/views.py +7 -18
  52. nautobot/dcim/apps.py +8 -4
  53. nautobot/dcim/elevations.py +5 -1
  54. nautobot/dcim/factory.py +7 -7
  55. nautobot/dcim/filters/__init__.py +16 -17
  56. nautobot/dcim/forms.py +61 -45
  57. nautobot/dcim/homepage.py +11 -3
  58. nautobot/dcim/management/commands/migrate_location_contacts.py +218 -0
  59. nautobot/dcim/migrations/0057_controller_models.py +11 -70
  60. nautobot/dcim/models/__init__.py +2 -2
  61. nautobot/dcim/models/devices.py +14 -16
  62. nautobot/dcim/models/racks.py +1 -3
  63. nautobot/dcim/navigation.py +23 -31
  64. nautobot/dcim/signals.py +6 -6
  65. nautobot/dcim/tables/__init__.py +2 -2
  66. nautobot/dcim/tables/devices.py +13 -16
  67. nautobot/dcim/tables/template_code.py +1 -1
  68. nautobot/dcim/templates/dcim/controller_create.html +70 -0
  69. nautobot/dcim/templates/dcim/controller_retrieve.html +35 -18
  70. nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +88 -0
  71. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +75 -43
  72. nautobot/dcim/templates/dcim/device.html +11 -3
  73. nautobot/dcim/templates/dcim/device_edit.html +1 -1
  74. nautobot/dcim/templates/dcim/devicefamily_retrieve.html +4 -0
  75. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +1 -1
  76. nautobot/dcim/tests/test_api.py +47 -6
  77. nautobot/dcim/tests/test_filters.py +92 -81
  78. nautobot/dcim/tests/test_graphql.py +11 -1
  79. nautobot/dcim/tests/test_models.py +15 -15
  80. nautobot/dcim/tests/test_signals.py +3 -1
  81. nautobot/dcim/tests/test_views.py +24 -12
  82. nautobot/dcim/urls.py +1 -1
  83. nautobot/dcim/views.py +25 -15
  84. nautobot/extras/api/serializers.py +20 -1
  85. nautobot/extras/api/urls.py +1 -2
  86. nautobot/extras/api/views.py +0 -10
  87. nautobot/extras/apps.py +7 -0
  88. nautobot/extras/context_managers.py +15 -4
  89. nautobot/extras/filters/__init__.py +53 -2
  90. nautobot/extras/filters/customfields.py +14 -9
  91. nautobot/extras/filters/mixins.py +6 -1
  92. nautobot/extras/forms/contacts.py +7 -0
  93. nautobot/extras/health_checks.py +1 -0
  94. nautobot/extras/jobs.py +1 -0
  95. nautobot/extras/managers.py +15 -2
  96. nautobot/extras/models/contacts.py +1 -0
  97. nautobot/extras/models/customfields.py +25 -2
  98. nautobot/extras/models/datasources.py +1 -0
  99. nautobot/extras/models/mixins.py +1 -0
  100. nautobot/extras/navigation.py +71 -65
  101. nautobot/extras/plugins/__init__.py +2 -1
  102. nautobot/extras/plugins/views.py +7 -11
  103. nautobot/extras/querysets.py +1 -2
  104. nautobot/extras/secrets/providers.py +1 -0
  105. nautobot/extras/signals.py +15 -5
  106. nautobot/extras/tasks.py +70 -17
  107. nautobot/extras/tests/test_api.py +2 -4
  108. nautobot/extras/tests/test_customfields.py +72 -9
  109. nautobot/extras/tests/test_dynamicgroups.py +2 -0
  110. nautobot/extras/tests/test_filters.py +89 -4
  111. nautobot/extras/tests/test_models.py +9 -0
  112. nautobot/extras/tests/test_relationships.py +10 -1
  113. nautobot/extras/tests/test_views.py +112 -1
  114. nautobot/extras/views.py +18 -17
  115. nautobot/ipam/api/serializers.py +10 -0
  116. nautobot/ipam/api/urls.py +1 -2
  117. nautobot/ipam/api/views.py +0 -11
  118. nautobot/ipam/apps.py +3 -2
  119. nautobot/ipam/tables.py +2 -22
  120. nautobot/ipam/tests/test_graphql.py +2 -3
  121. nautobot/ipam/tests/test_tables.py +42 -0
  122. nautobot/ipam/tests/test_views.py +1 -0
  123. nautobot/ipam/views.py +9 -9
  124. nautobot/project-static/css/base.css +1 -0
  125. nautobot/project-static/docs/404.html +126 -73
  126. nautobot/project-static/docs/apps/index.html +127 -71
  127. nautobot/project-static/docs/apps/nautobot-apps.html +127 -71
  128. nautobot/project-static/docs/assets/javascripts/{bundle.8fd75fb4.min.js → bundle.bd41221c.min.js} +2 -2
  129. nautobot/project-static/docs/assets/javascripts/{bundle.8fd75fb4.min.js.map → bundle.bd41221c.min.js.map} +3 -3
  130. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +1 -0
  131. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +1 -0
  132. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +127 -71
  133. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +127 -71
  134. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +167 -73
  135. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +165 -72
  136. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +127 -71
  137. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +127 -71
  138. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +127 -71
  139. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +127 -71
  140. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +127 -71
  141. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +127 -71
  142. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +127 -71
  143. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +127 -71
  144. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +127 -71
  145. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +127 -71
  146. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +127 -71
  147. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +127 -71
  148. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +127 -71
  149. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +127 -71
  150. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +128 -72
  151. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +127 -71
  152. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +127 -71
  153. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +345 -71
  154. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +172 -73
  155. nautobot/project-static/docs/development/apps/api/configuration-view.html +127 -71
  156. nautobot/project-static/docs/development/apps/api/database-backend-config.html +127 -71
  157. nautobot/project-static/docs/development/apps/api/models/django-admin.html +127 -71
  158. nautobot/project-static/docs/development/apps/api/models/global-search.html +127 -71
  159. nautobot/project-static/docs/development/apps/api/models/graphql.html +127 -71
  160. nautobot/project-static/docs/development/apps/api/models/index.html +127 -71
  161. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +127 -71
  162. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +127 -71
  163. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +127 -71
  164. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +127 -71
  165. nautobot/project-static/docs/development/apps/api/platform-features/index.html +127 -71
  166. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +127 -71
  167. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +127 -71
  168. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +127 -71
  169. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +127 -71
  170. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +127 -71
  171. nautobot/project-static/docs/development/apps/api/prometheus.html +127 -71
  172. nautobot/project-static/docs/development/apps/api/setup.html +127 -71
  173. nautobot/project-static/docs/development/apps/api/testing.html +127 -71
  174. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +127 -71
  175. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +127 -71
  176. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +127 -71
  177. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +127 -71
  178. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +127 -71
  179. nautobot/project-static/docs/development/apps/api/views/base-template.html +127 -71
  180. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +141 -80
  181. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +144 -83
  182. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +127 -71
  183. nautobot/project-static/docs/development/apps/api/views/index.html +127 -71
  184. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +127 -71
  185. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +127 -71
  186. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +127 -71
  187. nautobot/project-static/docs/development/apps/api/views/notes.html +127 -71
  188. nautobot/project-static/docs/development/apps/api/views/rest-api.html +127 -71
  189. nautobot/project-static/docs/development/apps/api/views/urls.html +127 -71
  190. nautobot/project-static/docs/development/apps/index.html +127 -71
  191. nautobot/project-static/docs/development/apps/migration/code-updates.html +127 -71
  192. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +127 -71
  193. nautobot/project-static/docs/development/apps/migration/from-v1.html +127 -71
  194. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +127 -71
  195. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +127 -71
  196. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +127 -71
  197. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +127 -71
  198. nautobot/project-static/docs/development/apps/porting-from-netbox.html +127 -71
  199. nautobot/project-static/docs/development/core/application-registry.html +127 -71
  200. nautobot/project-static/docs/development/core/best-practices.html +145 -79
  201. nautobot/project-static/docs/development/core/bootstrap-ui.html +127 -71
  202. nautobot/project-static/docs/development/core/caching.html +127 -71
  203. nautobot/project-static/docs/development/core/controllers.html +141 -275
  204. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +127 -71
  205. nautobot/project-static/docs/development/core/extending-models.html +13 -8166
  206. nautobot/project-static/docs/development/core/generic-views.html +142 -86
  207. nautobot/project-static/docs/development/core/getting-started.html +146 -81
  208. nautobot/project-static/docs/development/core/homepage.html +145 -89
  209. nautobot/project-static/docs/development/core/index.html +127 -71
  210. nautobot/project-static/docs/development/core/model-checklist.html +8354 -0
  211. nautobot/project-static/docs/development/core/model-features.html +130 -74
  212. nautobot/project-static/docs/development/core/natural-keys.html +127 -71
  213. nautobot/project-static/docs/development/core/navigation-menu.html +127 -71
  214. nautobot/project-static/docs/development/core/release-checklist.html +127 -71
  215. nautobot/project-static/docs/development/core/role-internals.html +127 -71
  216. nautobot/project-static/docs/development/core/settings.html +127 -71
  217. nautobot/project-static/docs/development/core/style-guide.html +127 -71
  218. nautobot/project-static/docs/development/core/templates.html +127 -71
  219. nautobot/project-static/docs/development/core/testing.html +127 -71
  220. nautobot/project-static/docs/development/core/user-preferences.html +127 -71
  221. nautobot/project-static/docs/development/extending-models.html +3 -3
  222. nautobot/project-static/docs/development/index.html +127 -71
  223. nautobot/project-static/docs/development/jobs/index.html +128 -72
  224. nautobot/project-static/docs/development/jobs/migration/from-v1.html +127 -71
  225. nautobot/project-static/docs/index.html +126 -73
  226. nautobot/project-static/docs/models/dcim/{controllerdevicegroup.html → controllermanageddevicegroup.html} +3 -3
  227. nautobot/project-static/docs/objects.inv +0 -0
  228. nautobot/project-static/docs/release-notes/index.html +127 -71
  229. nautobot/project-static/docs/release-notes/version-1.0.html +127 -71
  230. nautobot/project-static/docs/release-notes/version-1.1.html +127 -71
  231. nautobot/project-static/docs/release-notes/version-1.2.html +127 -71
  232. nautobot/project-static/docs/release-notes/version-1.3.html +127 -71
  233. nautobot/project-static/docs/release-notes/version-1.4.html +127 -71
  234. nautobot/project-static/docs/release-notes/version-1.5.html +127 -71
  235. nautobot/project-static/docs/release-notes/version-1.6.html +127 -71
  236. nautobot/project-static/docs/release-notes/version-2.0.html +127 -71
  237. nautobot/project-static/docs/release-notes/version-2.1.html +538 -254
  238. nautobot/project-static/docs/release-notes/version-2.2.html +520 -101
  239. nautobot/project-static/docs/requirements.txt +3 -3
  240. nautobot/project-static/docs/search/search_index.json +1 -1
  241. nautobot/project-static/docs/sitemap.xml +264 -259
  242. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  243. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +127 -71
  244. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +127 -71
  245. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +127 -71
  246. nautobot/project-static/docs/user-guide/administration/configuration/index.html +127 -71
  247. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +192 -71
  248. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +127 -71
  249. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +127 -71
  250. nautobot/project-static/docs/user-guide/administration/guides/caching.html +127 -71
  251. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +127 -71
  252. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +127 -71
  253. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +127 -71
  254. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +131 -71
  255. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +127 -71
  256. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +127 -71
  257. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +130 -74
  258. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +127 -71
  259. nautobot/project-static/docs/user-guide/administration/installation/docker.html +134 -74
  260. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +127 -71
  261. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +8616 -0
  262. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +127 -71
  263. nautobot/project-static/docs/user-guide/administration/installation/index.html +127 -71
  264. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +127 -71
  265. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +127 -71
  266. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +130 -74
  267. nautobot/project-static/docs/user-guide/administration/installation/services.html +127 -71
  268. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +127 -71
  269. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +127 -71
  270. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +127 -71
  271. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +127 -71
  272. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +127 -71
  273. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +127 -71
  274. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +127 -71
  275. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +127 -71
  276. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +127 -71
  277. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +127 -71
  278. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +127 -71
  279. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +127 -71
  280. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +127 -71
  281. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +127 -71
  282. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +127 -71
  283. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +127 -71
  284. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +127 -71
  285. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +127 -71
  286. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +127 -71
  287. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +127 -71
  288. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +127 -71
  289. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +127 -71
  290. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +127 -71
  291. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +362 -79
  292. nautobot/project-static/docs/user-guide/core-data-model/dcim/{controllerdevicegroup.html → controllermanageddevicegroup.html} +210 -85
  293. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +127 -71
  294. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +127 -71
  295. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +127 -71
  296. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +127 -71
  297. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +127 -71
  298. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +127 -71
  299. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +127 -71
  300. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +127 -71
  301. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +127 -71
  302. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +127 -71
  303. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +127 -71
  304. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +127 -71
  305. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +127 -71
  306. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +127 -71
  307. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +127 -71
  308. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +127 -71
  309. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +127 -71
  310. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +127 -71
  311. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +127 -71
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +127 -71
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +127 -71
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +127 -71
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +127 -71
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +127 -71
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +127 -71
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +127 -71
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +127 -71
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +127 -71
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +127 -71
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +127 -71
  323. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +130 -74
  324. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +127 -71
  325. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +138 -71
  326. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +138 -71
  327. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +127 -71
  328. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +127 -71
  329. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +127 -71
  330. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +127 -71
  331. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +127 -71
  332. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +127 -71
  333. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +127 -71
  334. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +127 -71
  335. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +127 -71
  336. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +127 -71
  337. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +127 -71
  338. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +127 -71
  339. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +127 -71
  340. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +127 -71
  341. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +127 -71
  342. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +127 -71
  343. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +127 -71
  344. nautobot/project-static/docs/user-guide/feature-guides/{contact-and-team.html → contacts-and-teams.html} +128 -72
  345. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +129 -73
  346. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +127 -71
  347. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +127 -71
  348. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +127 -71
  349. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +127 -71
  350. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +127 -71
  351. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +127 -71
  352. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +129 -73
  353. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +127 -71
  354. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +127 -71
  355. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +127 -71
  356. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +127 -71
  357. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +127 -71
  358. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +127 -71
  359. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +127 -71
  360. nautobot/project-static/docs/user-guide/index.html +127 -71
  361. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +127 -71
  362. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +127 -71
  363. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +127 -71
  364. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +127 -71
  365. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +127 -71
  366. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +127 -71
  367. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +127 -71
  368. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +127 -71
  369. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +127 -71
  370. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +127 -71
  371. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +127 -71
  372. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +127 -71
  373. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +127 -71
  374. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +127 -71
  375. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +127 -71
  376. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +127 -71
  377. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +127 -71
  378. nautobot/project-static/docs/user-guide/platform-functionality/note.html +127 -71
  379. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +127 -71
  380. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +127 -71
  381. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +127 -71
  382. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +127 -71
  383. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +127 -71
  384. nautobot/project-static/docs/user-guide/platform-functionality/role.html +127 -71
  385. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +127 -71
  386. nautobot/project-static/docs/user-guide/platform-functionality/status.html +127 -71
  387. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +127 -71
  388. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +127 -71
  389. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +127 -71
  390. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +127 -71
  391. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +127 -71
  392. nautobot/project-static/jquery/jquery-3.7.1.min.js +2 -0
  393. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_444444_256x240.png +0 -0
  394. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_555555_256x240.png +0 -0
  395. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_777620_256x240.png +0 -0
  396. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_777777_256x240.png +0 -0
  397. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_cc0000_256x240.png +0 -0
  398. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/images/ui-icons_ffffff_256x240.png +0 -0
  399. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +7 -0
  400. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +6 -0
  401. nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +5 -0
  402. nautobot/project-static/{jquery-ui-1.13.1 → jquery-ui-1.13.2}/jquery-ui.theme.min.css +1 -1
  403. nautobot/tenancy/api/urls.py +1 -2
  404. nautobot/tenancy/api/views.py +0 -12
  405. nautobot/tenancy/tables.py +1 -1
  406. nautobot/tenancy/tests/test_views.py +1 -0
  407. nautobot/users/api/urls.py +1 -2
  408. nautobot/users/api/views.py +2 -65
  409. nautobot/users/views.py +8 -8
  410. nautobot/virtualization/api/urls.py +1 -2
  411. nautobot/virtualization/api/views.py +0 -12
  412. {nautobot-2.2.0b1.dist-info → nautobot-2.2.1.dist-info}/METADATA +24 -24
  413. {nautobot-2.2.0b1.dist-info → nautobot-2.2.1.dist-info}/RECORD +418 -412
  414. nautobot/dcim/templates/dcim/controllerdevicegroup_create.html +0 -43
  415. nautobot/project-static/docs/assets/stylesheets/main.f2e4d321.min.css +0 -1
  416. nautobot/project-static/docs/assets/stylesheets/main.f2e4d321.min.css.map +0 -1
  417. nautobot/project-static/jquery/jquery-3.6.0.min.js +0 -2
  418. nautobot/project-static/jquery-ui-1.13.1/jquery-ui.min.css +0 -7
  419. nautobot/project-static/jquery-ui-1.13.1/jquery-ui.min.js +0 -6
  420. nautobot/project-static/jquery-ui-1.13.1/jquery-ui.structure.min.css +0 -5
  421. /nautobot/dcim/templates/dcim/{controllerdevicegroup_retrieve.html → controllermanageddevicegroup_retrieve.html} +0 -0
  422. {nautobot-2.2.0b1.dist-info → nautobot-2.2.1.dist-info}/LICENSE.txt +0 -0
  423. {nautobot-2.2.0b1.dist-info → nautobot-2.2.1.dist-info}/NOTICE +0 -0
  424. {nautobot-2.2.0b1.dist-info → nautobot-2.2.1.dist-info}/WHEEL +0 -0
  425. {nautobot-2.2.0b1.dist-info → nautobot-2.2.1.dist-info}/entry_points.txt +0 -0
@@ -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:
@@ -17,11 +17,21 @@ from nautobot.core.choices import ColorChoices
17
17
  from nautobot.core.models.fields import slugify_dashes_to_underscores
18
18
  from nautobot.core.testing import extract_form_failures, extract_page_body, TestCase, ViewTestCases
19
19
  from nautobot.core.testing.utils import disable_warnings, post_data
20
- from nautobot.dcim.models import ConsolePort, Device, DeviceType, Interface, Location, LocationType, Manufacturer
20
+ from nautobot.dcim.models import (
21
+ ConsolePort,
22
+ Controller,
23
+ Device,
24
+ DeviceType,
25
+ Interface,
26
+ Location,
27
+ LocationType,
28
+ Manufacturer,
29
+ )
21
30
  from nautobot.dcim.tests import test_views
22
31
  from nautobot.extras.choices import (
23
32
  CustomFieldTypeChoices,
24
33
  JobExecutionType,
34
+ LogLevelChoices,
25
35
  ObjectChangeActionChoices,
26
36
  SecretsGroupAccessTypeChoices,
27
37
  SecretsGroupSecretTypeChoices,
@@ -43,6 +53,7 @@ from nautobot.extras.models import (
43
53
  GraphQLQuery,
44
54
  Job,
45
55
  JobButton,
56
+ JobLogEntry,
46
57
  JobResult,
47
58
  Note,
48
59
  ObjectChange,
@@ -781,6 +792,49 @@ class DynamicGroupTestCase(
781
792
  "dynamic_group_memberships-MAX_NUM_FORMS": "1000",
782
793
  }
783
794
 
795
+ def test_get_object_dynamic_groups_anonymous(self):
796
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
797
+ self.client.logout()
798
+ response = self.client.get(url, follow=True)
799
+ self.assertHttpStatus(response, 200)
800
+ self.assertRedirects(response, f"/login/?next={url}")
801
+
802
+ def test_get_object_dynamic_groups_without_permission(self):
803
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
804
+ response = self.client.get(url)
805
+ self.assertHttpStatus(response, [403, 404])
806
+
807
+ def test_get_object_dynamic_groups_with_permission(self):
808
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
809
+ self.add_permissions("dcim.view_device", "extras.view_dynamicgroup")
810
+ response = self.client.get(url)
811
+ self.assertHttpStatus(response, 200)
812
+ response_body = response.content.decode(response.charset)
813
+ self.assertIn("DG 1", response_body, msg=response_body)
814
+ self.assertIn("DG 2", response_body, msg=response_body)
815
+ self.assertIn("DG 3", response_body, msg=response_body)
816
+
817
+ def test_get_object_dynamic_groups_with_constrained_permission(self):
818
+ self.add_permissions("extras.view_dynamicgroup")
819
+ obj_perm = ObjectPermission(
820
+ name="View a device",
821
+ constraints={"pk": Device.objects.first().pk},
822
+ actions=["view"],
823
+ )
824
+ obj_perm.save()
825
+ obj_perm.users.add(self.user)
826
+ obj_perm.object_types.add(ContentType.objects.get_for_model(Device))
827
+
828
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
829
+ response = self.client.get(url)
830
+ self.assertHttpStatus(response, 200)
831
+ response_body = response.content.decode(response.charset)
832
+ self.assertIn("DG 1", response_body, msg=response_body)
833
+
834
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.last().pk})
835
+ response = self.client.get(url)
836
+ self.assertHttpStatus(response, 404)
837
+
784
838
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
785
839
  def test_edit_saved_filter(self):
786
840
  """Test that editing a filter works using the edit view."""
@@ -936,6 +990,34 @@ class GitRepositoryTestCase(
936
990
  self.form_data = form_data
937
991
  super().test_edit_object_with_constrained_permission()
938
992
 
993
+ def test_post_sync_repo_anonymous(self):
994
+ self.client.logout()
995
+ url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
996
+ response = self.client.post(url, follow=True)
997
+ self.assertHttpStatus(response, 200)
998
+ self.assertRedirects(response, f"/login/?next={url}")
999
+
1000
+ def test_post_sync_repo_without_permission(self):
1001
+ url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
1002
+ response = self.client.post(url)
1003
+ self.assertHttpStatus(response, [403, 404])
1004
+
1005
+ # TODO: mock/stub out `enqueue_pull_git_repository_and_refresh_data` and test successful POST with permissions
1006
+
1007
+ def test_post_dryrun_repo_anonymous(self):
1008
+ self.client.logout()
1009
+ url = reverse("extras:gitrepository_dryrun", kwargs={"pk": self._get_queryset().first().pk})
1010
+ response = self.client.post(url, follow=True)
1011
+ self.assertHttpStatus(response, 200)
1012
+ self.assertRedirects(response, f"/login/?next={url}")
1013
+
1014
+ def test_post_dryrun_repo_without_permission(self):
1015
+ url = reverse("extras:gitrepository_dryrun", kwargs={"pk": self._get_queryset().first().pk})
1016
+ response = self.client.post(url)
1017
+ self.assertHttpStatus(response, [403, 404])
1018
+
1019
+ # TODO: mock/stub out `enqueue_git_repository_diff_origin_and_local` and test successful POST with permissions
1020
+
939
1021
 
940
1022
  class NoteTestCase(
941
1023
  ViewTestCases.CreateObjectViewTestCase,
@@ -1636,6 +1718,34 @@ class JobResultTestCase(
1636
1718
  def setUpTestData(cls):
1637
1719
  JobResult.objects.create(name="pass.TestPass")
1638
1720
  JobResult.objects.create(name="fail.TestFail")
1721
+ JobLogEntry.objects.create(
1722
+ log_level=LogLevelChoices.LOG_INFO,
1723
+ job_result=JobResult.objects.first(),
1724
+ grouping="run",
1725
+ message="This is a test",
1726
+ )
1727
+
1728
+ def test_get_joblogentrytable_anonymous(self):
1729
+ url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
1730
+ self.client.logout()
1731
+ response = self.client.get(url, follow=True)
1732
+ self.assertHttpStatus(response, 200)
1733
+ self.assertRedirects(response, f"/login/?next={url}")
1734
+
1735
+ def test_get_joblogentrytable_without_permission(self):
1736
+ url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
1737
+ response = self.client.get(url)
1738
+ self.assertHttpStatus(response, [403, 404])
1739
+
1740
+ def test_get_joblogentrytable_with_permission(self):
1741
+ url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
1742
+ self.add_permissions("extras.view_jobresult", "extras.view_joblogentry")
1743
+ response = self.client.get(url)
1744
+ self.assertHttpStatus(response, 200)
1745
+ response_body = response.content.decode(response.charset)
1746
+ self.assertIn("This is a test", response_body)
1747
+
1748
+ # TODO test with constrained permissions on both JobResult and JobLogEntry records
1639
1749
 
1640
1750
 
1641
1751
  class JobTestCase(
@@ -2488,6 +2598,7 @@ class RelationshipTestCase(
2488
2598
  )
2489
2599
 
2490
2600
  # Try deleting all devices and then editing the 6 VLANs (fails):
2601
+ Controller.objects.filter(controller_device__isnull=False).delete()
2491
2602
  Device.objects.all().delete()
2492
2603
  response = self.client.post(
2493
2604
  reverse("ipam:vlan_bulk_edit"), data={"pk": [str(vlan.id) for vlan in vlans], "_apply": [""]}
nautobot/extras/views.py CHANGED
@@ -40,6 +40,7 @@ from nautobot.core.views.viewsets import NautobotUIViewSet
40
40
  from nautobot.dcim.models import Controller, Device, Interface, Location, Rack
41
41
  from nautobot.dcim.tables import ControllerTable, DeviceTable, RackTable
42
42
  from nautobot.extras.constants import JOB_OVERRIDABLE_FIELDS
43
+ from nautobot.extras.signals import change_context_state
43
44
  from nautobot.extras.tasks import delete_custom_field_data
44
45
  from nautobot.extras.utils import get_base_template, get_worker_count
45
46
  from nautobot.ipam.models import IPAddress, Prefix, VLAN
@@ -369,7 +370,6 @@ class ContactUIViewSet(NautobotUIViewSet):
369
370
  queryset = Contact.objects.all()
370
371
  serializer_class = serializers.ContactSerializer
371
372
  table_class = tables.ContactTable
372
- is_contact_associatable_model = False
373
373
 
374
374
  def get_extra_context(self, request, instance):
375
375
  context = super().get_extra_context(request, instance)
@@ -677,8 +677,14 @@ class CustomFieldBulkDeleteView(generic.BulkDeleteView):
677
677
  """
678
678
  Helper method to construct a list of celery tasks to execute when bulk deleting custom fields.
679
679
  """
680
+ change_context = change_context_state.get()
681
+ if change_context is None:
682
+ context = None
683
+ else:
684
+ context = change_context.as_dict(queryset)
685
+ context["context_detail"] = "bulk delete custom field data"
680
686
  tasks = [
681
- delete_custom_field_data.si(obj.key, set(obj.content_types.values_list("pk", flat=True)))
687
+ delete_custom_field_data.si(obj.key, set(obj.content_types.values_list("pk", flat=True)), context)
682
688
  for obj in queryset
683
689
  ]
684
690
  return tasks
@@ -917,7 +923,7 @@ class DynamicGroupBulkDeleteView(generic.BulkDeleteView):
917
923
  filterset = filters.DynamicGroupFilterSet
918
924
 
919
925
 
920
- class ObjectDynamicGroupsView(View):
926
+ class ObjectDynamicGroupsView(generic.GenericView):
921
927
  """
922
928
  Present a list of dynamic groups associated to a particular object.
923
929
  base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
@@ -1116,18 +1122,18 @@ def check_and_call_git_repository_function(request, pk, func):
1116
1122
  messages.error(request, "Unable to run job: Celery worker process not running.")
1117
1123
  return redirect(request.get_full_path(), permanent=False)
1118
1124
  else:
1119
- repository = get_object_or_404(GitRepository, pk=pk)
1125
+ repository = get_object_or_404(GitRepository.objects.restrict(request.user, "change"), pk=pk)
1120
1126
  job_result = func(repository, request.user)
1121
1127
 
1122
1128
  return redirect(job_result.get_absolute_url())
1123
1129
 
1124
1130
 
1125
- class GitRepositorySyncView(View):
1131
+ class GitRepositorySyncView(generic.GenericView):
1126
1132
  def post(self, request, pk):
1127
1133
  return check_and_call_git_repository_function(request, pk, enqueue_pull_git_repository_and_refresh_data)
1128
1134
 
1129
1135
 
1130
- class GitRepositoryDryRunView(View):
1136
+ class GitRepositoryDryRunView(generic.GenericView):
1131
1137
  def post(self, request, pk):
1132
1138
  return check_and_call_git_repository_function(request, pk, enqueue_git_repository_diff_origin_and_local)
1133
1139
 
@@ -1806,7 +1812,7 @@ class JobResultView(generic.ObjectView):
1806
1812
  }
1807
1813
 
1808
1814
 
1809
- class JobLogEntryTableView(View):
1815
+ class JobLogEntryTableView(generic.GenericView):
1810
1816
  """
1811
1817
  Display a table of `JobLogEntry` objects for a given `JobResult` instance.
1812
1818
  """
@@ -1814,7 +1820,7 @@ class JobLogEntryTableView(View):
1814
1820
  queryset = JobResult.objects.all()
1815
1821
 
1816
1822
  def get(self, request, pk=None):
1817
- instance = self.queryset.get(pk=pk)
1823
+ instance = get_object_or_404(self.queryset.restrict(request.user, "view"), pk=pk)
1818
1824
  filter_q = request.GET.get("q")
1819
1825
  if filter_q:
1820
1826
  queryset = instance.job_log_entries.filter(
@@ -1893,7 +1899,7 @@ class ObjectChangeView(generic.ObjectView):
1893
1899
  }
1894
1900
 
1895
1901
 
1896
- class ObjectChangeLogView(View):
1902
+ class ObjectChangeLogView(generic.GenericView):
1897
1903
  """
1898
1904
  Present a history of changes made to a particular object.
1899
1905
  base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
@@ -1939,8 +1945,6 @@ class ObjectChangeLogView(View):
1939
1945
  "table": objectchanges_table,
1940
1946
  "base_template": self.base_template,
1941
1947
  "active_tab": "changelog",
1942
- # Currently only Contact and Team models are not contact_associatable.
1943
- "is_contact_associatable_model": type(obj) not in [Contact, Team],
1944
1948
  },
1945
1949
  )
1946
1950
 
@@ -1979,7 +1983,7 @@ class NoteDeleteView(generic.ObjectDeleteView):
1979
1983
  queryset = Note.objects.all()
1980
1984
 
1981
1985
 
1982
- class ObjectNotesView(View):
1986
+ class ObjectNotesView(generic.GenericView):
1983
1987
  """
1984
1988
  Present a list of notes associated to a particular object.
1985
1989
  base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
@@ -2000,7 +2004,7 @@ class ObjectNotesView(View):
2000
2004
  "assigned_object_id": obj.pk,
2001
2005
  }
2002
2006
  )
2003
- notes_table = tables.NoteTable(obj.notes)
2007
+ notes_table = tables.NoteTable(obj.notes.restrict(request.user, "view"))
2004
2008
 
2005
2009
  # Apply the request context
2006
2010
  paginate = {
@@ -2022,8 +2026,6 @@ class ObjectNotesView(View):
2022
2026
  "base_template": self.base_template,
2023
2027
  "active_tab": "notes",
2024
2028
  "form": notes_form,
2025
- # Currently only Contact and Team models are not contact_associatable.
2026
- "is_contact_associatable_model": type(obj) not in [Contact, Team],
2027
2029
  },
2028
2030
  )
2029
2031
 
@@ -2241,7 +2243,7 @@ class SecretView(generic.ObjectView):
2241
2243
  }
2242
2244
 
2243
2245
 
2244
- class SecretProviderParametersFormView(View):
2246
+ class SecretProviderParametersFormView(generic.GenericView):
2245
2247
  """
2246
2248
  Helper view to SecretView; retrieve the HTML form appropriate for entering parameters for a given SecretsProvider.
2247
2249
  """
@@ -2532,7 +2534,6 @@ class TeamUIViewSet(NautobotUIViewSet):
2532
2534
  queryset = Team.objects.all()
2533
2535
  serializer_class = serializers.TeamSerializer
2534
2536
  table_class = tables.TeamTable
2535
- is_contact_associatable_model = False
2536
2537
 
2537
2538
  def get_extra_context(self, request, instance):
2538
2539
  context = super().get_extra_context(request, instance)
@@ -495,6 +495,7 @@ class ServiceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
495
495
  class Meta:
496
496
  model = Service
497
497
  fields = "__all__"
498
+ validators = []
498
499
  extra_kwargs = {
499
500
  "device": {"help_text": "Required if no virtual_machine is specified"},
500
501
  "virtual_machine": {"help_text": "Required if no device is specified"},
@@ -504,3 +505,12 @@ class ServiceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
504
505
  # `device`.
505
506
  # list_display_fields = ["name", "parent", "protocol", "ports", "description"]
506
507
  list_display_fields = ["name", "device", "protocol", "ports", "description"]
508
+
509
+ def validate(self, data):
510
+ if data.get("device"):
511
+ validator = UniqueTogetherValidator(queryset=Service.objects.all(), fields=("name", "device"))
512
+ validator(data, self)
513
+ if data.get("virtual_machine"):
514
+ validator = UniqueTogetherValidator(queryset=Service.objects.all(), fields=("name", "virtual_machine"))
515
+ validator(data, self)
516
+ return super().validate(data)
nautobot/ipam/api/urls.py CHANGED
@@ -2,8 +2,7 @@ from nautobot.core.api.routers import OrderedDefaultRouter
2
2
 
3
3
  from . import views
4
4
 
5
- router = OrderedDefaultRouter()
6
- router.APIRootView = views.IPAMRootView
5
+ router = OrderedDefaultRouter(view_name="IPAM")
7
6
 
8
7
  # Namespaces
9
8
  router.register("namespaces", views.NamespaceViewSet)