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
@@ -770,6 +770,7 @@ class APIOrderingTestCase(testing.APITestCase):
770
770
  "TextField": "admin_contact",
771
771
  "DateTimeField": "created",
772
772
  }
773
+ cls.maxDiff = None
773
774
 
774
775
  def _validate_sorted_response(self, response, queryset, field_name, is_fk_field=False):
775
776
  self.assertHttpStatus(response, 200)
@@ -794,18 +795,24 @@ class APIOrderingTestCase(testing.APITestCase):
794
795
  """Tests that results are returned in the expected ascending order."""
795
796
 
796
797
  for field_type, field_name in self.field_type_map.items():
797
- with self.subTest(f"Testing {field_type}"):
798
- response = self.client.get(f"{self.url}?sort={field_name}&limit=10", **self.header)
799
- self._validate_sorted_response(response, Provider.objects.all().order_by(field_name), field_name)
798
+ with self.subTest(f"Testing {field_type} {field_name}"):
799
+ # Use `name` as a secondary sort as fields like `asn` and `admin_contact` may be null
800
+ response = self.client.get(f"{self.url}?sort={field_name},name&limit=10", **self.header)
801
+ self._validate_sorted_response(
802
+ response, Provider.objects.all().order_by(field_name, "name"), field_name
803
+ )
800
804
 
801
805
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
802
806
  def test_descending_sort(self):
803
807
  """Tests that results are returned in the expected descending order."""
804
808
 
805
809
  for field_type, field_name in self.field_type_map.items():
806
- with self.subTest(f"Testing {field_type}"):
807
- response = self.client.get(f"{self.url}?sort=-{field_name}&limit=10", **self.header)
808
- self._validate_sorted_response(response, Provider.objects.all().order_by(f"-{field_name}"), field_name)
810
+ with self.subTest(f"Testing {field_type} {field_name}"):
811
+ # Use `name` as a secondary sort as fields like `asn` and `admin_contact` may be null
812
+ response = self.client.get(f"{self.url}?sort=-{field_name},name&limit=10", **self.header)
813
+ self._validate_sorted_response(
814
+ response, Provider.objects.all().order_by(f"-{field_name}", "name"), field_name
815
+ )
809
816
 
810
817
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
811
818
  def test_sorting_tree_node_models(self):
@@ -4,7 +4,7 @@ from django.urls import reverse
4
4
 
5
5
  from nautobot.core.constants import CSV_NO_OBJECT, CSV_NULL_TYPE, VARBINARY_IP_FIELD_REPR_OF_CSV_NO_OBJECT
6
6
  from nautobot.dcim.api.serializers import DeviceSerializer
7
- from nautobot.dcim.models.devices import Device, DeviceType
7
+ from nautobot.dcim.models.devices import Controller, Device, DeviceType
8
8
  from nautobot.dcim.models.locations import Location
9
9
  from nautobot.extras.models.roles import Role
10
10
  from nautobot.extras.models.statuses import Status
@@ -25,6 +25,7 @@ class CSVParsingRelatedTestCase(TestCase):
25
25
  devicerole = Role.objects.get_for_model(Device).first()
26
26
  device_status = Status.objects.get_for_model(Device).first()
27
27
  tags = Tag.objects.get_for_model(Device).all()[:3]
28
+ Controller.objects.filter(controller_device__isnull=False).delete()
28
29
  Device.objects.all().delete()
29
30
  self.device = Device.objects.create(
30
31
  device_type=devicetype,
@@ -92,7 +93,7 @@ class CSVParsingRelatedTestCase(TestCase):
92
93
  "primary_ip6__host",
93
94
  "cluster__name",
94
95
  "virtual_chassis__name",
95
- "controller_device_group__name",
96
+ "controller_managed_device_group__name",
96
97
  "device_redundancy_group__name",
97
98
  "software_version__platform__name",
98
99
  "software_version__version",
@@ -119,7 +120,7 @@ class CSVParsingRelatedTestCase(TestCase):
119
120
  "primary_ip6",
120
121
  "cluster",
121
122
  "virtual_chassis",
122
- "controller_device_group",
123
+ "controller_managed_device_group",
123
124
  "device_redundancy_group",
124
125
  "secrets_group",
125
126
  ]
@@ -224,7 +225,7 @@ class CSVParsingRelatedTestCase(TestCase):
224
225
  "primary_ip6__host": CSV_NO_OBJECT,
225
226
  "cluster__name": CSV_NO_OBJECT,
226
227
  "virtual_chassis__name": CSV_NO_OBJECT,
227
- "controller_device_group__name": CSV_NO_OBJECT,
228
+ "controller_managed_device_group__name": CSV_NO_OBJECT,
228
229
  "device_redundancy_group__name": CSV_NO_OBJECT,
229
230
  "software_version__platform__name": CSV_NO_OBJECT,
230
231
  "software_version__version": CSV_NO_OBJECT,
@@ -17,7 +17,7 @@ from nautobot.core.constants import CHARFIELD_MAX_LENGTH
17
17
  from nautobot.core.models import fields as core_fields
18
18
  from nautobot.core.utils import lookup
19
19
  from nautobot.dcim import choices as dcim_choices, filters as dcim_filters, models as dcim_models
20
- from nautobot.dcim.models import Device
20
+ from nautobot.dcim.models import Controller, Device
21
21
  from nautobot.extras import models as extras_models
22
22
  from nautobot.extras.utils import FeatureQuery
23
23
  from nautobot.ipam import models as ipam_models
@@ -830,6 +830,7 @@ class DynamicFilterLookupExpressionTest(TestCase):
830
830
  @classmethod
831
831
  def setUpTestData(cls):
832
832
  manufacturers = dcim_models.Manufacturer.objects.all()[:3]
833
+ Controller.objects.filter(controller_device__isnull=False).delete()
833
834
  Device.objects.all().delete()
834
835
 
835
836
  device_types = (
@@ -43,6 +43,7 @@ from nautobot.dcim.models import (
43
43
  Cable,
44
44
  ConsolePort,
45
45
  ConsoleServerPort,
46
+ Controller,
46
47
  Device,
47
48
  DeviceType,
48
49
  FrontPort,
@@ -630,21 +631,9 @@ class GraphQLAPIPermissionTest(GraphQLTestCaseBase):
630
631
  self.assertEqual(names, ["Rack 1-1", "Rack 1-2", "Rack 2-1", "Rack 2-2"])
631
632
 
632
633
  def test_graphql_api_no_token(self):
633
- """Validate unauthenticated users are not able to query anything by default."""
634
+ """Validate unauthenticated users are not able to query anything."""
634
635
  response = self.client.post(self.api_url, {"query": self.get_racks_query}, format="json")
635
- self.assertEqual(response.status_code, status.HTTP_200_OK)
636
- self.assertIsInstance(response.data["data"]["racks"], list)
637
- names = [item["name"] for item in response.data["data"]["racks"]]
638
- self.assertEqual(names, [])
639
-
640
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
641
- def test_graphql_api_no_token_exempt(self):
642
- """Validate unauthenticated users are able to query based on the exempt permissions."""
643
- response = self.client.post(self.api_url, {"query": self.get_racks_query}, format="json")
644
- self.assertEqual(response.status_code, status.HTTP_200_OK)
645
- self.assertIsInstance(response.data["data"]["racks"], list)
646
- names = [item["name"] for item in response.data["data"]["racks"]]
647
- self.assertEqual(names, ["Rack 1-1", "Rack 1-2", "Rack 2-1", "Rack 2-2"])
636
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
648
637
 
649
638
  def test_graphql_api_wrong_token(self):
650
639
  """Validate a wrong token return 403."""
@@ -721,6 +710,7 @@ class GraphQLQueryTest(GraphQLTestCaseBase):
721
710
 
722
711
  # Remove random IPAddress and Device fixtures for this custom test
723
712
  IPAddress.objects.all().delete()
713
+ Controller.objects.filter(controller_device__isnull=False).delete()
724
714
  Device.objects.all().delete()
725
715
 
726
716
  # Initialize fake request that will be required to execute GraphQL query
@@ -44,6 +44,8 @@ class NavMenuTestCase(TestCase):
44
44
  expected_name = "Interfaces"
45
45
  elif expected_name == "Object Changes":
46
46
  expected_name = "Change Log"
47
+ elif expected_name == "Controller Managed Device Groups":
48
+ expected_name = "Managed Device Groups"
47
49
  self.assertEqual(item_details["name"], expected_name)
48
50
  if item_url == get_route_for_model(view_model, "list"):
49
51
  # Not assertEqual as some menu items have additional permissions defined.
@@ -87,6 +89,7 @@ class NavMenuTestCase(TestCase):
87
89
  self.assertEqual(expected_perms[tab_name], tab_details["permissions"])
88
90
 
89
91
 
92
+ @tag("unit")
90
93
  class NewUINavTest(TestCase):
91
94
  @patch.dict(registry, values={"new_ui_nav_menu": {}}, clear=True)
92
95
  def test_build_new_ui_nav_menu(self):
@@ -2,6 +2,7 @@ import re
2
2
  from unittest import mock
3
3
  import urllib.parse
4
4
 
5
+ from django.apps import apps
5
6
  from django.contrib.contenttypes.models import ContentType
6
7
  from django.core.files.uploadedfile import SimpleUploadedFile
7
8
  from django.test import override_settings, RequestFactory
@@ -9,6 +10,7 @@ from django.test.utils import override_script_prefix
9
10
  from django.urls import get_script_prefix, reverse
10
11
  from prometheus_client.parser import text_string_to_metric_families
11
12
 
13
+ from nautobot.core.constants import GLOBAL_SEARCH_EXCLUDE_LIST
12
14
  from nautobot.core.testing import TestCase
13
15
  from nautobot.core.testing.api import APITestCase
14
16
  from nautobot.core.utils.permissions import get_permission_for_model
@@ -71,6 +73,27 @@ class HomeViewTestCase(TestCase):
71
73
  response = self.client.get(f"{url}?{urllib.parse.urlencode(params)}")
72
74
  self.assertHttpStatus(response, 200)
73
75
 
76
+ def test_appropriate_models_included_in_global_search(self):
77
+ # Gather core app configs
78
+ existing_models = []
79
+ global_searchable_models = []
80
+ for app_name in ["circuits", "dcim", "extras", "ipam", "tenancy", "virtualization"]:
81
+ app_config = apps.get_app_config(app_name)
82
+ existing_models += [model._meta.model_name for model in app_config.get_models()]
83
+ global_searchable_models += app_config.searchable_models
84
+
85
+ # Remove those models that are not searchable
86
+ existing_models = [model for model in existing_models if model not in GLOBAL_SEARCH_EXCLUDE_LIST]
87
+ existing_models.sort()
88
+
89
+ # See if there are any models that are missing from global search
90
+ difference = [model for model in existing_models if model not in global_searchable_models]
91
+ if difference:
92
+ self.fail(
93
+ f'Existing model/models {",".join(difference)} are not included in the searchable_models attribute of the app config.\n'
94
+ 'If you do not want the models to be searchable, please include them in the GLOBAL_SEARCH_EXCLUDE_LIST constant in nautobot.core.constants.'
95
+ )
96
+
74
97
  def make_request(self):
75
98
  url = reverse("home")
76
99
  response = self.client.get(url)
@@ -305,25 +328,31 @@ class LoginUITestCase(TestCase):
305
328
  sso_login_search_result = self.make_request()
306
329
  self.assertIsNotNone(sso_login_search_result)
307
330
 
308
- @override_settings(BANNER_TOP="Hello, Banner Top", BANNER_BOTTOM="Hello, Banner Bottom")
309
- def test_routes_redirect_back_to_login_unauthenticated(self):
310
- """Assert that api docs and graphql redirects to login page if user is unauthenticated."""
331
+ def test_graphql_redirects_back_to_login_unauthenticated(self):
332
+ """Assert that graphql redirects to login page if user is unauthenticated."""
311
333
  self.client.logout()
312
334
  headers = {"HTTP_ACCEPT": "text/html"}
313
- urls = [reverse("api_docs"), reverse("graphql")]
335
+ url = reverse("graphql")
336
+ response = self.client.get(url, follow=True, **headers)
337
+ self.assertHttpStatus(response, 200)
338
+ self.assertRedirects(response, f"/login/?next={url}")
339
+ response_content = response.content.decode(response.charset).replace("\n", "")
340
+ for footer_text in self.footer_elements:
341
+ self.assertNotIn(footer_text, response_content)
342
+
343
+ def test_api_docs_403_unauthenticated(self):
344
+ """Assert that api docs return a 403 Forbidden if user is unauthenticated."""
345
+ self.client.logout()
346
+ urls = [
347
+ reverse("api_docs"),
348
+ reverse("api_redocs"),
349
+ reverse("schema"),
350
+ reverse("schema_json"),
351
+ reverse("schema_yaml"),
352
+ ]
314
353
  for url in urls:
315
- response = self.client.get(url, follow=True, **headers)
316
- self.assertHttpStatus(response, 200)
317
- redirect_chain = [(f"/login/?next={url}", 302)]
318
- self.assertEqual(response.redirect_chain, redirect_chain)
319
- response_content = response.content.decode(response.charset).replace("\n", "")
320
- # Assert Footer items(`self.footer_elements`), Banner and Banner Top is hidden
321
- for footer_text in self.footer_elements:
322
- self.assertNotIn(footer_text, response_content)
323
- # Only API Docs implements BANNERS
324
- if url == urls[0]:
325
- self.assertNotIn("Hello, Banner Top", response_content)
326
- self.assertNotIn("Hello, Banner Bottom", response_content)
354
+ response = self.client.get(url)
355
+ self.assertHttpStatus(response, 403)
327
356
 
328
357
 
329
358
  class MetricsViewTestCase(TestCase):
@@ -51,8 +51,7 @@ def flatten_iterable(iterable):
51
51
  """
52
52
  for i in iterable:
53
53
  if hasattr(i, "__iter__") and not isinstance(i, str):
54
- for j in flatten_iterable(i):
55
- yield j
54
+ yield from flatten_iterable(i)
56
55
  else:
57
56
  yield i
58
57
 
@@ -1,12 +1,14 @@
1
1
  """Utilities for looking up related classes and information."""
2
2
 
3
3
  import inspect
4
+ import re
4
5
 
5
6
  from django.apps import apps
6
7
  from django.conf import settings
7
8
  from django.contrib.auth.models import Group
8
9
  from django.contrib.contenttypes.models import ContentType
9
10
  from django.db.models import Model
11
+ from django.urls import get_resolver, URLPattern, URLResolver
10
12
  from django.utils.module_loading import import_string
11
13
 
12
14
 
@@ -232,3 +234,127 @@ def get_created_and_last_updated_usernames_for_model(instance):
232
234
  last_updated_by = last_updated_by_record.user_name
233
235
 
234
236
  return created_by, last_updated_by
237
+
238
+
239
+ def get_url_patterns(urlconf=None, patterns_list=None, base_path="/"):
240
+ """
241
+ Recursively yield a list of registered URL patterns.
242
+
243
+ Args:
244
+ urlconf (URLConf): Python module such as `nautobot.core.urls`.
245
+ Default if unspecified is the value of `settings.ROOT_URLCONF`, i.e. the `nautobot.core.urls` module.
246
+ patterns_list (list): Used in recursion. Generally can be omitted on initial call.
247
+ Default if unspecified is the `url_patterns` attribute of the given `urlconf` module.
248
+ base_path (str): String to prepend to all URL patterns yielded.
249
+ Default if unspecified is the string `"/"`.
250
+
251
+ Yields:
252
+ (str): Each URL pattern defined in the given urlconf and its descendants
253
+
254
+ Examples:
255
+ >>> generator = get_url_patterns()
256
+ >>> next(generator)
257
+ '/'
258
+ >>> next(generator)
259
+ '/search/'
260
+ >>> next(generator)
261
+ '/login/'
262
+ >>> next(generator)
263
+ '/logout/'
264
+ >>> next(generator)
265
+ '/circuits/circuits/<uuid:pk>/terminations/swap/'
266
+
267
+ >>> import example_plugin.urls as example_urls
268
+ >>> for url_pattern in get_url_patterns(example_urls, base_path="/plugins/example-app/"):
269
+ ... print(url_pattern)
270
+ ...
271
+ /plugins/example-app/
272
+ /plugins/example-app/config/
273
+ /plugins/example-app/models/<uuid:pk>/dynamic-groups/
274
+ /plugins/example-app/other-models/<uuid:pk>/dynamic-groups/
275
+ /plugins/example-app/docs/
276
+ /plugins/example-app/circuits/<uuid:pk>/example-app-tab/
277
+ /plugins/example-app/devices/<uuid:pk>/example-app-tab-1/
278
+ /plugins/example-app/devices/<uuid:pk>/example-app-tab-2/
279
+ /plugins/example-app/override-target/
280
+ /plugins/example-app/^models/$
281
+ /plugins/example-app/^models/add/$
282
+ /plugins/example-app/^models/import/$
283
+ /plugins/example-app/^models/edit/$
284
+ /plugins/example-app/^models/delete/$
285
+ /plugins/example-app/^models/all-names/$
286
+ /plugins/example-app/^models/(?P<pk>[^/.]+)/$
287
+ /plugins/example-app/^models/(?P<pk>[^/.]+)/delete/$
288
+ /plugins/example-app/^models/(?P<pk>[^/.]+)/edit/$
289
+ /plugins/example-app/^models/(?P<pk>[^/.]+)/changelog/$
290
+ /plugins/example-app/^models/(?P<pk>[^/.]+)/notes/$
291
+ /plugins/example-app/^other-models/$
292
+ /plugins/example-app/^other-models/add/$
293
+ /plugins/example-app/^other-models/edit/$
294
+ /plugins/example-app/^other-models/delete/$
295
+ /plugins/example-app/^other-models/(?P<pk>[^/.]+)/$
296
+ /plugins/example-app/^other-models/(?P<pk>[^/.]+)/delete/$
297
+ /plugins/example-app/^other-models/(?P<pk>[^/.]+)/edit/$
298
+ /plugins/example-app/^other-models/(?P<pk>[^/.]+)/changelog/$
299
+ /plugins/example-app/^other-models/(?P<pk>[^/.]+)/notes/$
300
+ """
301
+ if urlconf is None:
302
+ urlconf = settings.ROOT_URLCONF
303
+ if patterns_list is None:
304
+ patterns_list = get_resolver(urlconf).url_patterns
305
+
306
+ for item in patterns_list:
307
+ if isinstance(item, URLPattern):
308
+ yield base_path + str(item.pattern)
309
+ elif isinstance(item, URLResolver):
310
+ # Recurse!
311
+ yield from get_url_patterns(urlconf, item.url_patterns, base_path + str(item.pattern))
312
+
313
+
314
+ def get_url_for_url_pattern(url_pattern):
315
+ """
316
+ Given a URL pattern, construct a URL string that would match that pattern.
317
+
318
+ Examples:
319
+ >>> get_url_for_url_pattern("/plugins/example-app/^models/(?P<pk>[^/.]+)/$")
320
+ '/plugins/example-app/models/00000000-0000-0000-0000-000000000000/'
321
+ >>> get_url_for_url_pattern("/circuits/circuit-terminations/<uuid:termination_a_id>/connect/<str:termination_b_type>/")
322
+ '/circuits/circuit-terminations/00000000-0000-0000-0000-000000000000/connect/string/'
323
+ """
324
+ url = url_pattern
325
+ # Fixup tokens in path-style "classic" view URLs:
326
+ # "/admin/users/user/<id>/password/"
327
+ url = re.sub(r"<id>", "00000000-0000-0000-0000-000000000000", url)
328
+ # "/silk/request/<uuid:request_id>/profile/<int:profile_id>/"
329
+ url = re.sub(r"<int:\w+>", "1", url)
330
+ # "/admin/admin/logentry/<path:object_id>/"
331
+ url = re.sub(r"<path:\w+>", "1", url)
332
+ # "/dcim/sites/<slug:slug>/"
333
+ url = re.sub(r"<slug:\w+>", "slug", url)
334
+ # "/apps/installed-apps/<str:app>/"
335
+ url = re.sub(r"<str:\w+>", "string", url)
336
+ # "/dcim/locations/<uuid:pk>/"
337
+ url = re.sub(r"<uuid:\w+>", "00000000-0000-0000-0000-000000000000", url)
338
+ # "/api/circuits/<drf_format_suffix:format>"
339
+ url = re.sub(r"<drf_format_suffix:\w+>", ".json", url)
340
+ # tokens in regexp-style router urls, including REST and NautobotUIViewSet:
341
+ # "/extras/^external-integrations/(?P<pk>[^/.]+)/$"
342
+ # "/api/virtualization/^interfaces/(?P<pk>[^/.]+)/$"
343
+ # "/api/virtualization/^interfaces/(?P<pk>[^/.]+)\\.(?P<format>[a-z0-9]+)/?$"
344
+ url = re.sub(r"[$^]", "", url)
345
+ url = re.sub(r"/\?", "/", url)
346
+ url = re.sub(r"\(\?P<app_label>[^)]+\)", "users", url)
347
+ url = re.sub(r"\(\?P<class_path>[^)]+\)", "foo/bar/baz", url)
348
+ url = re.sub(r"\(\?P<format>[^)]+\)", "json", url)
349
+ url = re.sub(r"\(\?P<name>[^)]+\)", "string", url)
350
+ url = re.sub(r"\(\?P<pk>[^)]+\)", "00000000-0000-0000-0000-000000000000", url)
351
+ url = re.sub(r"\(\?P<slug>[^)]+\)", "string", url)
352
+ url = re.sub(r"\(\?P<url>[^)]+\)", "any", url)
353
+ # Fallthru for generic URL parameters
354
+ url = re.sub(r"\(\?P<\w+>[^)]+\)\??", "unknown", url)
355
+ url = re.sub(r"\\", "", url)
356
+
357
+ if any(char in url for char in "<>[]()?+^$"):
358
+ raise RuntimeError(f"Unhandled token in URL {url} derived from {url_pattern}")
359
+
360
+ return url
@@ -11,7 +11,7 @@ from django.contrib.auth.decorators import permission_required
11
11
  from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin
12
12
  from django.contrib.contenttypes.models import ContentType
13
13
  from django.http import HttpResponseForbidden, HttpResponseServerError, JsonResponse
14
- from django.shortcuts import get_object_or_404, redirect, render
14
+ from django.shortcuts import get_object_or_404, render
15
15
  from django.template import loader, RequestContext, Template
16
16
  from django.template.exceptions import TemplateDoesNotExist
17
17
  from django.urls import resolve, reverse
@@ -210,7 +210,7 @@ class SearchView(AccessMixin, View):
210
210
  )
211
211
 
212
212
 
213
- class StaticMediaFailureView(View):
213
+ class StaticMediaFailureView(View): # NOT using LoginRequiredMixin here as this may happen even on the login page
214
214
  """
215
215
  Display a user-friendly error message with troubleshooting tips when a static media file fails to load.
216
216
  """
@@ -265,12 +265,8 @@ def csrf_failure(request, reason="", template_name="403_csrf_failure.html"):
265
265
  return HttpResponseForbidden(t.render(context), content_type="text/html")
266
266
 
267
267
 
268
- class CustomGraphQLView(GraphQLView):
268
+ class CustomGraphQLView(LoginRequiredMixin, GraphQLView):
269
269
  def render_graphiql(self, request, **data):
270
- if not request.user.is_authenticated:
271
- graphql_url = reverse("graphql")
272
- login_url = reverse(settings.LOGIN_URL)
273
- return redirect(f"{login_url}?next={graphql_url}")
274
270
  query_name = request.GET.get("name")
275
271
  if query_name:
276
272
  data["obj"] = GraphQLQuery.objects.get(name=query_name)
@@ -4,6 +4,7 @@ import re
4
4
 
5
5
  from django.conf import settings
6
6
  from django.contrib import messages
7
+ from django.contrib.auth.mixins import LoginRequiredMixin
7
8
  from django.contrib.contenttypes.models import ContentType
8
9
  from django.core.exceptions import (
9
10
  FieldDoesNotExist,
@@ -53,9 +54,18 @@ from nautobot.core.views.utils import (
53
54
  import_csv_helper,
54
55
  prepare_cloned_fields,
55
56
  )
57
+ from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
56
58
  from nautobot.extras.models import ContactAssociation, ExportTemplate
57
59
  from nautobot.extras.tables import AssociatedContactsTable
58
- from nautobot.extras.utils import remove_prefix_from_cf_key
60
+ from nautobot.extras.utils import bulk_delete_with_bulk_change_logging, remove_prefix_from_cf_key
61
+
62
+
63
+ class GenericView(LoginRequiredMixin, View):
64
+ """
65
+ Base class for non-object-related views.
66
+
67
+ Enforces authentication, which Django's base View does not by default.
68
+ """
59
69
 
60
70
 
61
71
  class ObjectView(ObjectPermissionRequiredMixin, View):
@@ -68,7 +78,6 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
68
78
 
69
79
  queryset = None
70
80
  template_name = None
71
- is_contact_associatable_model = True
72
81
 
73
82
  def get_required_permission(self):
74
83
  return get_permission_for_model(self.queryset.model, "view")
@@ -127,7 +136,6 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
127
136
  content_type = ContentType.objects.get_for_model(self.queryset.model)
128
137
  context = {
129
138
  "object": instance,
130
- "is_contact_associatable_model": self.is_contact_associatable_model,
131
139
  "content_type": content_type,
132
140
  "verbose_name": self.queryset.model._meta.verbose_name,
133
141
  "verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
@@ -135,7 +143,7 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
135
143
  "last_updated_by": last_updated_by,
136
144
  **self.get_extra_context(request, instance),
137
145
  }
138
- if self.is_contact_associatable_model:
146
+ if instance.is_contact_associable_model:
139
147
  paginate = {"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
140
148
  associations = (
141
149
  ContactAssociation.objects.filter(
@@ -220,6 +228,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
220
228
  display_filter_params = []
221
229
  dynamic_filter_form = None
222
230
  filter_form = None
231
+ hide_hierarchy_ui = False
223
232
 
224
233
  if self.filterset:
225
234
  filter_params = self.get_filter_params(request)
@@ -232,6 +241,12 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
232
241
  )
233
242
  self.queryset = self.queryset.none()
234
243
 
244
+ # If a valid filterset is applied, we have to hide the hierarchy indentation in the UI for tables that support hierarchy indentation.
245
+ # NOTE: An empty filterset query-param is also valid filterset and we dont want to hide hierarchy indentation if no filter query-param is provided
246
+ # hence `filterset.data`.
247
+ if filterset.is_valid() and filterset.data:
248
+ hide_hierarchy_ui = True
249
+
235
250
  display_filter_params = [
236
251
  check_filter_for_display(filterset.filters, field_name, values)
237
252
  for field_name, values in filter_params.items()
@@ -283,9 +298,9 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
283
298
  table_config_form = None
284
299
  if self.table:
285
300
  # Construct the objects table
286
- # Order By is needed in the table `__init__` method
287
- order_by = self.request.GET.getlist("sort")
288
- table = self.table(self.queryset, user=request.user, order_by=order_by)
301
+ if self.request.GET.getlist("sort"):
302
+ hide_hierarchy_ui = True # hide tree hierarchy if custom sort is used
303
+ table = self.table(self.queryset, user=request.user, hide_hierarchy_ui=hide_hierarchy_ui)
289
304
  if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
290
305
  table.columns.show("pk")
291
306
 
@@ -974,7 +989,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
974
989
  nullified_fields = request.POST.getlist("_nullify")
975
990
 
976
991
  try:
977
- with transaction.atomic():
992
+ with deferred_change_logging_for_bulk_operation():
978
993
  updated_objects = []
979
994
  for obj in self.queryset.filter(pk__in=form.cleaned_data["pk"]):
980
995
  obj = self.alter_obj(obj, request, [], kwargs)
@@ -1238,13 +1253,12 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
1238
1253
 
1239
1254
  self.perform_pre_delete(request, queryset)
1240
1255
  try:
1241
- _, deleted_info = queryset.delete()
1256
+ _, deleted_info = bulk_delete_with_bulk_change_logging(queryset)
1242
1257
  deleted_count = deleted_info[model._meta.label]
1243
1258
  except ProtectedError as e:
1244
1259
  logger.info("Caught ProtectedError while attempting to delete objects")
1245
1260
  handle_protectederror(queryset, request, e)
1246
1261
  return redirect(self.get_return_url(request))
1247
-
1248
1262
  msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
1249
1263
  logger.info(msg)
1250
1264
  messages.success(request, msg)
@@ -45,10 +45,11 @@ from nautobot.core.views.utils import (
45
45
  import_csv_helper,
46
46
  prepare_cloned_fields,
47
47
  )
48
+ from nautobot.extras.context_managers import deferred_change_logging_for_bulk_operation
48
49
  from nautobot.extras.forms import NoteForm
49
50
  from nautobot.extras.models import ExportTemplate
50
51
  from nautobot.extras.tables import NoteTable, ObjectChangeTable
51
- from nautobot.extras.utils import remove_prefix_from_cf_key
52
+ from nautobot.extras.utils import bulk_delete_with_bulk_change_logging, remove_prefix_from_cf_key
52
53
 
53
54
  PERMISSIONS_ACTION_MAP = {
54
55
  "list": "view",
@@ -226,7 +227,6 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
226
227
  create_form_class = None
227
228
  update_form_class = None
228
229
  parser_classes = [FormParser, MultiPartParser]
229
- is_contact_associatable_model = True
230
230
  queryset = None
231
231
  # serializer_class has to be specified to eliminate the need to override retrieve() in the RetrieveModelMixin for now.
232
232
  serializer_class = None
@@ -608,6 +608,7 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
608
608
  action_buttons = ("add", "import", "export")
609
609
  filterset_class = None
610
610
  filterset_form_class = None
611
+ hide_hierarchy_ui = False
611
612
  non_filter_params = (
612
613
  "export", # trigger for CSV/export-template/YAML export # 3.0 TODO: remove, irrelevant after #4746
613
614
  "page", # used by django-tables2.RequestConfig
@@ -629,6 +630,12 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
629
630
  format_html("Invalid filters were specified: {}", self.filterset.errors),
630
631
  )
631
632
  queryset = queryset.none()
633
+
634
+ # If a valid filterset is applied, we have to hide the hierarchy indentation in the UI for tables that support hierarchy indentation.
635
+ # NOTE: An empty filterset query-param is also valid filterset and we dont want to hide hierarchy indentation if no filter query-param is provided
636
+ # hence `filterset.data`.
637
+ if self.filterset.is_valid() and self.filterset.data:
638
+ self.hide_hierarchy_ui = True
632
639
  return queryset
633
640
 
634
641
  # 3.0 TODO: remove, irrelevant after #4746
@@ -840,7 +847,7 @@ class ObjectBulkDestroyViewMixin(NautobotViewSetMixin, BulkDestroyModelMixin):
840
847
 
841
848
  try:
842
849
  with transaction.atomic():
843
- deleted_count = queryset.delete()[1][model._meta.label]
850
+ deleted_count = bulk_delete_with_bulk_change_logging(queryset)[1][model._meta.label]
844
851
  msg = f"Deleted {deleted_count} {model._meta.verbose_name_plural}"
845
852
  self.logger.info(msg)
846
853
  self.success_url = self.get_return_url(request)
@@ -972,7 +979,7 @@ class ObjectBulkUpdateViewMixin(NautobotViewSetMixin, BulkUpdateModelMixin):
972
979
  if field not in form_custom_fields + form_relationships + ["pk"] + ["object_note"]
973
980
  ]
974
981
  nullified_fields = request.POST.getlist("_nullify")
975
- with transaction.atomic():
982
+ with deferred_change_logging_for_bulk_operation():
976
983
  updated_objects = []
977
984
  for obj in queryset.filter(pk__in=form.cleaned_data["pk"]):
978
985
  self.obj = obj
@@ -65,10 +65,13 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
65
65
  table_class = view.get_table_class()
66
66
  request = kwargs.get("request", view.request)
67
67
  queryset = view.alter_queryset(request)
68
+
68
69
  if view.action in ["list", "notes", "changelog"]:
69
70
  if view.action == "list":
70
71
  permissions = kwargs.get("permissions", {})
71
- table = table_class(queryset, user=request.user)
72
+ if view.request.GET.getlist("sort"):
73
+ view.hide_hierarchy_ui = True # hide tree hierarchy if custom sort is used
74
+ table = table_class(queryset, user=request.user, hide_hierarchy_ui=view.hide_hierarchy_ui)
72
75
  if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
73
76
  table.columns.show("pk")
74
77
  elif view.action == "notes":
@@ -142,7 +145,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
142
145
  view = renderer_context["view"]
143
146
  request = renderer_context["request"]
144
147
  # Check if queryset attribute is set before doing anything
145
- is_contact_associatable_model = view.is_contact_associatable_model
146
148
  queryset = view.alter_queryset(request)
147
149
  model = queryset.model
148
150
  form_class = view.get_form_class()
@@ -219,7 +221,6 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
219
221
 
220
222
  context = {
221
223
  "content_type": content_type,
222
- "is_contact_associatable_model": is_contact_associatable_model,
223
224
  "form": form,
224
225
  "filter_form": filter_form,
225
226
  "dynamic_filter_form": self.get_dynamic_filter_form(view, request, filterset_class=view.filterset_class),
@@ -241,9 +242,13 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
241
242
 
242
243
  context["created_by"] = created_by
243
244
  context["last_updated_by"] = last_updated_by
244
- associated_contacts = instance.associated_contacts.restrict(request.user, "view").order_by("role__name")
245
- if is_contact_associatable_model:
246
- context["associated_contacts_table"] = AssociatedContactsTable(data=associated_contacts)
245
+ if instance.is_contact_associable_model:
246
+ paginate = {"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
247
+ associations = instance.associated_contacts.restrict(request.user, "view").order_by("role__name")
248
+ associations_table = AssociatedContactsTable(associations, orderable=False)
249
+ RequestConfig(request, paginate).configure(associations_table)
250
+ associations_table.columns.show("pk")
251
+ context["associated_contacts_table"] = associations_table
247
252
  else:
248
253
  context["associated_contacts_table"] = None
249
254
  context.update(view.get_extra_context(request, instance))