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
@@ -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,26 @@ 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.\nIf you do not want the models to be searchable, please include them in the GLOBAL_SEARCH_EXCLUDE_LIST constant in nautobot.core.constants.'
94
+ )
95
+
74
96
  def make_request(self):
75
97
  url = reverse("home")
76
98
  response = self.client.get(url)
@@ -305,25 +327,31 @@ class LoginUITestCase(TestCase):
305
327
  sso_login_search_result = self.make_request()
306
328
  self.assertIsNotNone(sso_login_search_result)
307
329
 
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."""
330
+ def test_graphql_redirects_back_to_login_unauthenticated(self):
331
+ """Assert that graphql redirects to login page if user is unauthenticated."""
311
332
  self.client.logout()
312
333
  headers = {"HTTP_ACCEPT": "text/html"}
313
- urls = [reverse("api_docs"), reverse("graphql")]
334
+ url = reverse("graphql")
335
+ response = self.client.get(url, follow=True, **headers)
336
+ self.assertHttpStatus(response, 200)
337
+ self.assertRedirects(response, f"/login/?next={url}")
338
+ response_content = response.content.decode(response.charset).replace("\n", "")
339
+ for footer_text in self.footer_elements:
340
+ self.assertNotIn(footer_text, response_content)
341
+
342
+ def test_api_docs_403_unauthenticated(self):
343
+ """Assert that api docs return a 403 Forbidden if user is unauthenticated."""
344
+ self.client.logout()
345
+ urls = [
346
+ reverse("api_docs"),
347
+ reverse("api_redocs"),
348
+ reverse("schema"),
349
+ reverse("schema_json"),
350
+ reverse("schema_yaml"),
351
+ ]
314
352
  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)
353
+ response = self.client.get(url)
354
+ self.assertHttpStatus(response, 403)
327
355
 
328
356
 
329
357
  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,
@@ -58,6 +59,14 @@ from nautobot.extras.tables import AssociatedContactsTable
58
59
  from nautobot.extras.utils import remove_prefix_from_cf_key
59
60
 
60
61
 
62
+ class GenericView(LoginRequiredMixin, View):
63
+ """
64
+ Base class for non-object-related views.
65
+
66
+ Enforces authentication, which Django's base View does not by default.
67
+ """
68
+
69
+
61
70
  class ObjectView(ObjectPermissionRequiredMixin, View):
62
71
  """
63
72
  Retrieve a single object for display.
@@ -68,7 +77,6 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
68
77
 
69
78
  queryset = None
70
79
  template_name = None
71
- is_contact_associatable_model = True
72
80
 
73
81
  def get_required_permission(self):
74
82
  return get_permission_for_model(self.queryset.model, "view")
@@ -127,7 +135,6 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
127
135
  content_type = ContentType.objects.get_for_model(self.queryset.model)
128
136
  context = {
129
137
  "object": instance,
130
- "is_contact_associatable_model": self.is_contact_associatable_model,
131
138
  "content_type": content_type,
132
139
  "verbose_name": self.queryset.model._meta.verbose_name,
133
140
  "verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
@@ -135,7 +142,7 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
135
142
  "last_updated_by": last_updated_by,
136
143
  **self.get_extra_context(request, instance),
137
144
  }
138
- if self.is_contact_associatable_model:
145
+ if instance.is_contact_associable_model:
139
146
  paginate = {"paginator_class": EnhancedPaginator, "per_page": get_paginate_count(request)}
140
147
  associations = (
141
148
  ContactAssociation.objects.filter(
@@ -220,6 +227,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
220
227
  display_filter_params = []
221
228
  dynamic_filter_form = None
222
229
  filter_form = None
230
+ hide_hierarchy_ui = False
223
231
 
224
232
  if self.filterset:
225
233
  filter_params = self.get_filter_params(request)
@@ -232,6 +240,12 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
232
240
  )
233
241
  self.queryset = self.queryset.none()
234
242
 
243
+ # If a valid filterset is applied, we have to hide the hierarchy indentation in the UI for tables that support hierarchy indentation.
244
+ # 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
245
+ # hence `filterset.data`.
246
+ if filterset.is_valid() and filterset.data:
247
+ hide_hierarchy_ui = True
248
+
235
249
  display_filter_params = [
236
250
  check_filter_for_display(filterset.filters, field_name, values)
237
251
  for field_name, values in filter_params.items()
@@ -283,9 +297,9 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
283
297
  table_config_form = None
284
298
  if self.table:
285
299
  # 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)
300
+ if self.request.GET.getlist("sort"):
301
+ hide_hierarchy_ui = True # hide tree hierarchy if custom sort is used
302
+ table = self.table(self.queryset, user=request.user, hide_hierarchy_ui=hide_hierarchy_ui)
289
303
  if "pk" in table.base_columns and (permissions["change"] or permissions["delete"]):
290
304
  table.columns.show("pk")
291
305
 
@@ -226,7 +226,6 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
226
226
  create_form_class = None
227
227
  update_form_class = None
228
228
  parser_classes = [FormParser, MultiPartParser]
229
- is_contact_associatable_model = True
230
229
  queryset = None
231
230
  # serializer_class has to be specified to eliminate the need to override retrieve() in the RetrieveModelMixin for now.
232
231
  serializer_class = None
@@ -608,6 +607,7 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
608
607
  action_buttons = ("add", "import", "export")
609
608
  filterset_class = None
610
609
  filterset_form_class = None
610
+ hide_hierarchy_ui = False
611
611
  non_filter_params = (
612
612
  "export", # trigger for CSV/export-template/YAML export # 3.0 TODO: remove, irrelevant after #4746
613
613
  "page", # used by django-tables2.RequestConfig
@@ -629,6 +629,12 @@ class ObjectListViewMixin(NautobotViewSetMixin, mixins.ListModelMixin):
629
629
  format_html("Invalid filters were specified: {}", self.filterset.errors),
630
630
  )
631
631
  queryset = queryset.none()
632
+
633
+ # If a valid filterset is applied, we have to hide the hierarchy indentation in the UI for tables that support hierarchy indentation.
634
+ # 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
635
+ # hence `filterset.data`.
636
+ if self.filterset.is_valid() and self.filterset.data:
637
+ self.hide_hierarchy_ui = True
632
638
  return queryset
633
639
 
634
640
  # 3.0 TODO: remove, irrelevant after #4746
@@ -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))
@@ -55,7 +55,7 @@ from nautobot.dcim.models import (
55
55
  ConsoleServerPort,
56
56
  ConsoleServerPortTemplate,
57
57
  Controller,
58
- ControllerDeviceGroup,
58
+ ControllerManagedDeviceGroup,
59
59
  Device,
60
60
  DeviceBay,
61
61
  DeviceBayTemplate,
@@ -598,7 +598,7 @@ class DeviceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
598
598
  "secrets_group",
599
599
  "device_redundancy_group",
600
600
  "device_redundancy_group_priority",
601
- "controller_device_group",
601
+ "controller_managed_device_group",
602
602
  ]
603
603
  },
604
604
  },
@@ -1049,7 +1049,7 @@ class ControllerSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
1049
1049
  fields = "__all__"
1050
1050
 
1051
1051
 
1052
- class ControllerDeviceGroupSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
1052
+ class ControllerManagedDeviceGroupSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
1053
1053
  class Meta:
1054
- model = ControllerDeviceGroup
1054
+ model = ControllerManagedDeviceGroup
1055
1055
  fields = "__all__"
nautobot/dcim/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.DCIMRootView
5
+ router = OrderedDefaultRouter(view_name="DCIM")
7
6
 
8
7
  # Locations
9
8
  router.register("location-types", views.LocationTypeViewSet)
@@ -80,7 +79,7 @@ router.register("connected-device", views.ConnectedDeviceViewSet, basename="conn
80
79
 
81
80
  # Controllers
82
81
  router.register("controllers", views.ControllerViewSet)
83
- router.register("controller-device-groups", views.ControllerDeviceGroupViewSet)
82
+ router.register("controller-managed-device-groups", views.ControllerManagedDeviceGroupViewSet)
84
83
 
85
84
  app_name = "dcim-api"
86
85
  urlpatterns = router.urls