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
@@ -17,11 +17,21 @@ from nautobot.core.choices import ColorChoices
17
17
  from nautobot.core.models.fields import slugify_dashes_to_underscores
18
18
  from nautobot.core.testing import extract_form_failures, extract_page_body, TestCase, ViewTestCases
19
19
  from nautobot.core.testing.utils import disable_warnings, post_data
20
- from nautobot.dcim.models import ConsolePort, Device, DeviceType, Interface, Location, LocationType, Manufacturer
20
+ from nautobot.dcim.models import (
21
+ ConsolePort,
22
+ Controller,
23
+ Device,
24
+ DeviceType,
25
+ Interface,
26
+ Location,
27
+ LocationType,
28
+ Manufacturer,
29
+ )
21
30
  from nautobot.dcim.tests import test_views
22
31
  from nautobot.extras.choices import (
23
32
  CustomFieldTypeChoices,
24
33
  JobExecutionType,
34
+ LogLevelChoices,
25
35
  ObjectChangeActionChoices,
26
36
  SecretsGroupAccessTypeChoices,
27
37
  SecretsGroupSecretTypeChoices,
@@ -43,6 +53,7 @@ from nautobot.extras.models import (
43
53
  GraphQLQuery,
44
54
  Job,
45
55
  JobButton,
56
+ JobLogEntry,
46
57
  JobResult,
47
58
  Note,
48
59
  ObjectChange,
@@ -781,6 +792,49 @@ class DynamicGroupTestCase(
781
792
  "dynamic_group_memberships-MAX_NUM_FORMS": "1000",
782
793
  }
783
794
 
795
+ def test_get_object_dynamic_groups_anonymous(self):
796
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
797
+ self.client.logout()
798
+ response = self.client.get(url, follow=True)
799
+ self.assertHttpStatus(response, 200)
800
+ self.assertRedirects(response, f"/login/?next={url}")
801
+
802
+ def test_get_object_dynamic_groups_without_permission(self):
803
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
804
+ response = self.client.get(url)
805
+ self.assertHttpStatus(response, [403, 404])
806
+
807
+ def test_get_object_dynamic_groups_with_permission(self):
808
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
809
+ self.add_permissions("dcim.view_device", "extras.view_dynamicgroup")
810
+ response = self.client.get(url)
811
+ self.assertHttpStatus(response, 200)
812
+ response_body = response.content.decode(response.charset)
813
+ self.assertIn("DG 1", response_body, msg=response_body)
814
+ self.assertIn("DG 2", response_body, msg=response_body)
815
+ self.assertIn("DG 3", response_body, msg=response_body)
816
+
817
+ def test_get_object_dynamic_groups_with_constrained_permission(self):
818
+ self.add_permissions("extras.view_dynamicgroup")
819
+ obj_perm = ObjectPermission(
820
+ name="View a device",
821
+ constraints={"pk": Device.objects.first().pk},
822
+ actions=["view"],
823
+ )
824
+ obj_perm.save()
825
+ obj_perm.users.add(self.user)
826
+ obj_perm.object_types.add(ContentType.objects.get_for_model(Device))
827
+
828
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
829
+ response = self.client.get(url)
830
+ self.assertHttpStatus(response, 200)
831
+ response_body = response.content.decode(response.charset)
832
+ self.assertIn("DG 1", response_body, msg=response_body)
833
+
834
+ url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.last().pk})
835
+ response = self.client.get(url)
836
+ self.assertHttpStatus(response, 404)
837
+
784
838
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
785
839
  def test_edit_saved_filter(self):
786
840
  """Test that editing a filter works using the edit view."""
@@ -936,6 +990,34 @@ class GitRepositoryTestCase(
936
990
  self.form_data = form_data
937
991
  super().test_edit_object_with_constrained_permission()
938
992
 
993
+ def test_post_sync_repo_anonymous(self):
994
+ self.client.logout()
995
+ url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
996
+ response = self.client.post(url, follow=True)
997
+ self.assertHttpStatus(response, 200)
998
+ self.assertRedirects(response, f"/login/?next={url}")
999
+
1000
+ def test_post_sync_repo_without_permission(self):
1001
+ url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
1002
+ response = self.client.post(url)
1003
+ self.assertHttpStatus(response, [403, 404])
1004
+
1005
+ # TODO: mock/stub out `enqueue_pull_git_repository_and_refresh_data` and test successful POST with permissions
1006
+
1007
+ def test_post_dryrun_repo_anonymous(self):
1008
+ self.client.logout()
1009
+ url = reverse("extras:gitrepository_dryrun", kwargs={"pk": self._get_queryset().first().pk})
1010
+ response = self.client.post(url, follow=True)
1011
+ self.assertHttpStatus(response, 200)
1012
+ self.assertRedirects(response, f"/login/?next={url}")
1013
+
1014
+ def test_post_dryrun_repo_without_permission(self):
1015
+ url = reverse("extras:gitrepository_dryrun", kwargs={"pk": self._get_queryset().first().pk})
1016
+ response = self.client.post(url)
1017
+ self.assertHttpStatus(response, [403, 404])
1018
+
1019
+ # TODO: mock/stub out `enqueue_git_repository_diff_origin_and_local` and test successful POST with permissions
1020
+
939
1021
 
940
1022
  class NoteTestCase(
941
1023
  ViewTestCases.CreateObjectViewTestCase,
@@ -1636,6 +1718,34 @@ class JobResultTestCase(
1636
1718
  def setUpTestData(cls):
1637
1719
  JobResult.objects.create(name="pass.TestPass")
1638
1720
  JobResult.objects.create(name="fail.TestFail")
1721
+ JobLogEntry.objects.create(
1722
+ log_level=LogLevelChoices.LOG_INFO,
1723
+ job_result=JobResult.objects.first(),
1724
+ grouping="run",
1725
+ message="This is a test",
1726
+ )
1727
+
1728
+ def test_get_joblogentrytable_anonymous(self):
1729
+ url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
1730
+ self.client.logout()
1731
+ response = self.client.get(url, follow=True)
1732
+ self.assertHttpStatus(response, 200)
1733
+ self.assertRedirects(response, f"/login/?next={url}")
1734
+
1735
+ def test_get_joblogentrytable_without_permission(self):
1736
+ url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
1737
+ response = self.client.get(url)
1738
+ self.assertHttpStatus(response, [403, 404])
1739
+
1740
+ def test_get_joblogentrytable_with_permission(self):
1741
+ url = reverse("extras:jobresult_log-table", kwargs={"pk": JobResult.objects.first().pk})
1742
+ self.add_permissions("extras.view_jobresult", "extras.view_joblogentry")
1743
+ response = self.client.get(url)
1744
+ self.assertHttpStatus(response, 200)
1745
+ response_body = response.content.decode(response.charset)
1746
+ self.assertIn("This is a test", response_body)
1747
+
1748
+ # TODO test with constrained permissions on both JobResult and JobLogEntry records
1639
1749
 
1640
1750
 
1641
1751
  class JobTestCase(
@@ -2488,6 +2598,7 @@ class RelationshipTestCase(
2488
2598
  )
2489
2599
 
2490
2600
  # Try deleting all devices and then editing the 6 VLANs (fails):
2601
+ Controller.objects.filter(controller_device__isnull=False).delete()
2491
2602
  Device.objects.all().delete()
2492
2603
  response = self.client.post(
2493
2604
  reverse("ipam:vlan_bulk_edit"), data={"pk": [str(vlan.id) for vlan in vlans], "_apply": [""]}
nautobot/extras/utils.py CHANGED
@@ -19,7 +19,9 @@ from nautobot.core.choices import ColorChoices
19
19
  from nautobot.core.constants import CHARFIELD_MAX_LENGTH
20
20
  from nautobot.core.models.managers import TagsManager
21
21
  from nautobot.core.models.utils import find_models_with_matching_fields
22
+ from nautobot.extras.choices import ObjectChangeActionChoices
22
23
  from nautobot.extras.constants import (
24
+ CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL,
23
25
  EXTRAS_FEATURES,
24
26
  JOB_MAX_NAME_LENGTH,
25
27
  JOB_OVERRIDABLE_FIELDS,
@@ -610,3 +612,38 @@ def migrate_role_data(
610
612
  model_to_migrate._meta.label,
611
613
  to_role_field_name,
612
614
  )
615
+
616
+
617
+ def bulk_delete_with_bulk_change_logging(qs, batch_size=1000):
618
+ """
619
+ Deletes objects in the provided queryset and creates ObjectChange instances in bulk to improve performance.
620
+ For use with bulk delete views. This operation is wrapped in an atomic transaction.
621
+ """
622
+ from nautobot.extras.models import ObjectChange
623
+ from nautobot.extras.signals import change_context_state
624
+
625
+ change_context = change_context_state.get()
626
+ if change_context is None:
627
+ raise ValueError("Change logging must be enabled before using bulk_delete_with_bulk_change_logging")
628
+
629
+ with transaction.atomic():
630
+ try:
631
+ queued_object_changes = []
632
+ change_context.defer_object_changes = True
633
+ for obj in qs.iterator():
634
+ if not hasattr(obj, "to_objectchange"):
635
+ break
636
+ if len(queued_object_changes) >= batch_size:
637
+ ObjectChange.objects.bulk_create(queued_object_changes)
638
+ queued_object_changes = []
639
+ oc = obj.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
640
+ oc.user = change_context.user
641
+ oc.request_id = change_context.change_id
642
+ oc.change_context = change_context.context
643
+ oc.change_context_detail = change_context.context_detail[:CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL]
644
+ queued_object_changes.append(oc)
645
+ ObjectChange.objects.bulk_create(queued_object_changes)
646
+ return qs.delete()
647
+ finally:
648
+ change_context.defer_object_changes = False
649
+ change_context.reset_deferred_object_changes()
nautobot/extras/views.py CHANGED
@@ -40,6 +40,7 @@ from nautobot.core.views.viewsets import NautobotUIViewSet
40
40
  from nautobot.dcim.models import Controller, Device, Interface, Location, Rack
41
41
  from nautobot.dcim.tables import ControllerTable, DeviceTable, RackTable
42
42
  from nautobot.extras.constants import JOB_OVERRIDABLE_FIELDS
43
+ from nautobot.extras.signals import change_context_state
43
44
  from nautobot.extras.tasks import delete_custom_field_data
44
45
  from nautobot.extras.utils import get_base_template, get_worker_count
45
46
  from nautobot.ipam.models import IPAddress, Prefix, VLAN
@@ -369,7 +370,6 @@ class ContactUIViewSet(NautobotUIViewSet):
369
370
  queryset = Contact.objects.all()
370
371
  serializer_class = serializers.ContactSerializer
371
372
  table_class = tables.ContactTable
372
- is_contact_associatable_model = False
373
373
 
374
374
  def get_extra_context(self, request, instance):
375
375
  context = super().get_extra_context(request, instance)
@@ -677,8 +677,14 @@ class CustomFieldBulkDeleteView(generic.BulkDeleteView):
677
677
  """
678
678
  Helper method to construct a list of celery tasks to execute when bulk deleting custom fields.
679
679
  """
680
+ change_context = change_context_state.get()
681
+ if change_context is None:
682
+ context = None
683
+ else:
684
+ context = change_context.as_dict(queryset)
685
+ context["context_detail"] = "bulk delete custom field data"
680
686
  tasks = [
681
- delete_custom_field_data.si(obj.key, set(obj.content_types.values_list("pk", flat=True)))
687
+ delete_custom_field_data.si(obj.key, set(obj.content_types.values_list("pk", flat=True)), context)
682
688
  for obj in queryset
683
689
  ]
684
690
  return tasks
@@ -917,7 +923,7 @@ class DynamicGroupBulkDeleteView(generic.BulkDeleteView):
917
923
  filterset = filters.DynamicGroupFilterSet
918
924
 
919
925
 
920
- class ObjectDynamicGroupsView(View):
926
+ class ObjectDynamicGroupsView(generic.GenericView):
921
927
  """
922
928
  Present a list of dynamic groups associated to a particular object.
923
929
  base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
@@ -1116,18 +1122,18 @@ def check_and_call_git_repository_function(request, pk, func):
1116
1122
  messages.error(request, "Unable to run job: Celery worker process not running.")
1117
1123
  return redirect(request.get_full_path(), permanent=False)
1118
1124
  else:
1119
- repository = get_object_or_404(GitRepository, pk=pk)
1125
+ repository = get_object_or_404(GitRepository.objects.restrict(request.user, "change"), pk=pk)
1120
1126
  job_result = func(repository, request.user)
1121
1127
 
1122
1128
  return redirect(job_result.get_absolute_url())
1123
1129
 
1124
1130
 
1125
- class GitRepositorySyncView(View):
1131
+ class GitRepositorySyncView(generic.GenericView):
1126
1132
  def post(self, request, pk):
1127
1133
  return check_and_call_git_repository_function(request, pk, enqueue_pull_git_repository_and_refresh_data)
1128
1134
 
1129
1135
 
1130
- class GitRepositoryDryRunView(View):
1136
+ class GitRepositoryDryRunView(generic.GenericView):
1131
1137
  def post(self, request, pk):
1132
1138
  return check_and_call_git_repository_function(request, pk, enqueue_git_repository_diff_origin_and_local)
1133
1139
 
@@ -1806,7 +1812,7 @@ class JobResultView(generic.ObjectView):
1806
1812
  }
1807
1813
 
1808
1814
 
1809
- class JobLogEntryTableView(View):
1815
+ class JobLogEntryTableView(generic.GenericView):
1810
1816
  """
1811
1817
  Display a table of `JobLogEntry` objects for a given `JobResult` instance.
1812
1818
  """
@@ -1814,7 +1820,7 @@ class JobLogEntryTableView(View):
1814
1820
  queryset = JobResult.objects.all()
1815
1821
 
1816
1822
  def get(self, request, pk=None):
1817
- instance = self.queryset.get(pk=pk)
1823
+ instance = get_object_or_404(self.queryset.restrict(request.user, "view"), pk=pk)
1818
1824
  filter_q = request.GET.get("q")
1819
1825
  if filter_q:
1820
1826
  queryset = instance.job_log_entries.filter(
@@ -1893,7 +1899,7 @@ class ObjectChangeView(generic.ObjectView):
1893
1899
  }
1894
1900
 
1895
1901
 
1896
- class ObjectChangeLogView(View):
1902
+ class ObjectChangeLogView(generic.GenericView):
1897
1903
  """
1898
1904
  Present a history of changes made to a particular object.
1899
1905
  base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
@@ -1939,8 +1945,6 @@ class ObjectChangeLogView(View):
1939
1945
  "table": objectchanges_table,
1940
1946
  "base_template": self.base_template,
1941
1947
  "active_tab": "changelog",
1942
- # Currently only Contact and Team models are not contact_associatable.
1943
- "is_contact_associatable_model": type(obj) not in [Contact, Team],
1944
1948
  },
1945
1949
  )
1946
1950
 
@@ -1979,7 +1983,7 @@ class NoteDeleteView(generic.ObjectDeleteView):
1979
1983
  queryset = Note.objects.all()
1980
1984
 
1981
1985
 
1982
- class ObjectNotesView(View):
1986
+ class ObjectNotesView(generic.GenericView):
1983
1987
  """
1984
1988
  Present a list of notes associated to a particular object.
1985
1989
  base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
@@ -2000,7 +2004,7 @@ class ObjectNotesView(View):
2000
2004
  "assigned_object_id": obj.pk,
2001
2005
  }
2002
2006
  )
2003
- notes_table = tables.NoteTable(obj.notes)
2007
+ notes_table = tables.NoteTable(obj.notes.restrict(request.user, "view"))
2004
2008
 
2005
2009
  # Apply the request context
2006
2010
  paginate = {
@@ -2022,8 +2026,6 @@ class ObjectNotesView(View):
2022
2026
  "base_template": self.base_template,
2023
2027
  "active_tab": "notes",
2024
2028
  "form": notes_form,
2025
- # Currently only Contact and Team models are not contact_associatable.
2026
- "is_contact_associatable_model": type(obj) not in [Contact, Team],
2027
2029
  },
2028
2030
  )
2029
2031
 
@@ -2241,7 +2243,7 @@ class SecretView(generic.ObjectView):
2241
2243
  }
2242
2244
 
2243
2245
 
2244
- class SecretProviderParametersFormView(View):
2246
+ class SecretProviderParametersFormView(generic.GenericView):
2245
2247
  """
2246
2248
  Helper view to SecretView; retrieve the HTML form appropriate for entering parameters for a given SecretsProvider.
2247
2249
  """
@@ -2532,7 +2534,6 @@ class TeamUIViewSet(NautobotUIViewSet):
2532
2534
  queryset = Team.objects.all()
2533
2535
  serializer_class = serializers.TeamSerializer
2534
2536
  table_class = tables.TeamTable
2535
- is_contact_associatable_model = False
2536
2537
 
2537
2538
  def get_extra_context(self, request, instance):
2538
2539
  context = super().get_extra_context(request, instance)
@@ -495,6 +495,7 @@ class ServiceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
495
495
  class Meta:
496
496
  model = Service
497
497
  fields = "__all__"
498
+ validators = []
498
499
  extra_kwargs = {
499
500
  "device": {"help_text": "Required if no virtual_machine is specified"},
500
501
  "virtual_machine": {"help_text": "Required if no device is specified"},
@@ -504,3 +505,12 @@ class ServiceSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
504
505
  # `device`.
505
506
  # list_display_fields = ["name", "parent", "protocol", "ports", "description"]
506
507
  list_display_fields = ["name", "device", "protocol", "ports", "description"]
508
+
509
+ def validate(self, data):
510
+ if data.get("device"):
511
+ validator = UniqueTogetherValidator(queryset=Service.objects.all(), fields=("name", "device"))
512
+ validator(data, self)
513
+ if data.get("virtual_machine"):
514
+ validator = UniqueTogetherValidator(queryset=Service.objects.all(), fields=("name", "virtual_machine"))
515
+ validator(data, self)
516
+ return super().validate(data)
nautobot/ipam/api/urls.py CHANGED
@@ -2,8 +2,7 @@ from nautobot.core.api.routers import OrderedDefaultRouter
2
2
 
3
3
  from . import views
4
4
 
5
- router = OrderedDefaultRouter()
6
- router.APIRootView = views.IPAMRootView
5
+ router = OrderedDefaultRouter(view_name="IPAM")
7
6
 
8
7
  # Namespaces
9
8
  router.register("namespaces", views.NamespaceViewSet)
@@ -6,7 +6,6 @@ from rest_framework import status
6
6
  from rest_framework.decorators import action
7
7
  from rest_framework.exceptions import APIException
8
8
  from rest_framework.response import Response
9
- from rest_framework.routers import APIRootView
10
9
 
11
10
  from nautobot.core.models.querysets import count_related
12
11
  from nautobot.core.utils.config import get_settings_or_config
@@ -32,16 +31,6 @@ from nautobot.ipam.models import (
32
31
 
33
32
  from . import serializers
34
33
 
35
-
36
- class IPAMRootView(APIRootView):
37
- """
38
- IPAM API root view
39
- """
40
-
41
- def get_view_name(self):
42
- return "IPAM"
43
-
44
-
45
34
  #
46
35
  # Namespace
47
36
  #
nautobot/ipam/apps.py CHANGED
@@ -5,10 +5,11 @@ class IPAMConfig(NautobotConfig):
5
5
  name = "nautobot.ipam"
6
6
  verbose_name = "IPAM"
7
7
  searchable_models = [
8
- "vrf",
9
- "prefix",
10
8
  "ipaddress",
9
+ "namespace",
10
+ "prefix",
11
11
  "vlan",
12
+ "vrf",
12
13
  ]
13
14
 
14
15
  def ready(self):
nautobot/ipam/tables.py CHANGED
@@ -42,28 +42,10 @@ UTILIZATION_GRAPH = """
42
42
  {% if record.present_in_database %}{% utilization_graph record.get_utilization %}{% else %}&mdash;{% endif %}
43
43
  """
44
44
 
45
- PREFIX_LINK = """
46
- {% load helpers %}
47
- {% for i in record.ancestors.count|as_range %}
48
- <i class="mdi mdi-circle-small"></i>
49
- {% endfor %}
50
- <a href="\
51
- {% if record.present_in_database %}\
52
- {% url 'ipam:prefix' pk=record.pk %}\
53
- {% else %}\
54
- {% url 'ipam:prefix_add' %}\
55
- ?prefix={{ record }}&namespace={{ object.namespace.pk }}\
56
- {% for loc in object.locations.all %}&locations={{ loc.pk }}{% endfor %}\
57
- {% if object.tenant %}&tenant_group={{ object.tenant.tenant_group.pk }}&tenant={{ object.tenant.pk }}{% endif %}\
58
- {% endif %}\
59
- ">{{ record.prefix }}</a>
60
- """
61
45
 
62
46
  PREFIX_COPY_LINK = """
63
47
  {% load helpers %}
64
- {% for i in record.ancestors.count|as_range %}
65
- <i class="mdi mdi-circle-small"></i>
66
- {% endfor %}
48
+ {% tree_hierarchy_ui_representation record.ancestors.count|as_range table.hide_hierarchy_ui%}
67
49
  <span class="hover_copy">
68
50
  <a href="\
69
51
  {% if record.present_in_database %}\
@@ -179,7 +161,6 @@ VLAN_LINK = """
179
161
  {% url 'ipam:vlan_add' %}\
180
162
  ?vid={{ record.vid }}&vlan_group={{ vlan_group.pk }}\
181
163
  {% if vlan_group.location %}&location={{ vlan_group.location.pk }}{% endif %}\
182
- {% if vlan_group.location %}&location={{ vlan_group.location.pk }}{% endif %}\
183
164
  " class="btn btn-xs btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a>\
184
165
  {% else %}
185
166
  {{ record.available }} VLAN{{ record.available|pluralize }} available
@@ -369,7 +350,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
369
350
  namespace = tables.Column(linkify=True)
370
351
  vlan = tables.Column(linkify=True, verbose_name="VLAN")
371
352
  rir = tables.Column(linkify=True)
372
- children = tables.Column(accessor="descendants_count")
353
+ children = tables.Column(accessor="descendants_count", orderable=False)
373
354
  date_allocated = tables.DateTimeColumn()
374
355
  location_count = LinkedCountColumn(
375
356
  viewname="dcim:location_list", url_params={"prefixes": "pk"}, verbose_name="Locations"
@@ -377,7 +358,6 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
377
358
 
378
359
  class Meta(BaseTable.Meta):
379
360
  model = Prefix
380
- orderable = False
381
361
  fields = (
382
362
  "pk",
383
363
  "prefix",
@@ -791,7 +771,7 @@ class InterfaceVLANTable(StatusTableMixin, BaseTable):
791
771
  tenant = TenantColumn()
792
772
  role = tables.TemplateColumn(template_code=VLAN_ROLE_LINK)
793
773
  location_count = LinkedCountColumn(
794
- viewname="dcim:location",
774
+ viewname="dcim:location_list",
795
775
  url_params={"vlans": "pk"},
796
776
  verbose_name="Locations",
797
777
  )
@@ -1,4 +1,3 @@
1
- from django.test import override_settings
2
1
  from django.urls import reverse
3
2
  from rest_framework import status
4
3
 
@@ -9,8 +8,8 @@ class TestPrefix(APITestCase):
9
8
  def setUp(self):
10
9
  super().setUp()
11
10
  self.api_url = reverse("graphql-api")
11
+ self.add_permissions("ipam.view_prefix")
12
12
 
13
- @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
14
13
  def test_prefix_ip_version(self):
15
14
  """Test ip_version is available for a Prefix via GraphQL."""
16
15
  get_prefixes_query = """
@@ -23,7 +22,7 @@ class TestPrefix(APITestCase):
23
22
  }
24
23
  """
25
24
  payload = {"query": get_prefixes_query}
26
- response = self.client.post(self.api_url, payload, format="json")
25
+ response = self.client.post(self.api_url, payload, format="json", **self.header)
27
26
  self.assertEqual(response.status_code, status.HTTP_200_OK)
28
27
  prefixes = response.data["data"]["prefixes"]
29
28
  self.assertIsInstance(prefixes, list)
@@ -0,0 +1,42 @@
1
+ from django.test import TestCase
2
+
3
+ from nautobot.core.models.querysets import count_related
4
+ from nautobot.dcim.models.locations import Location
5
+ from nautobot.ipam.models import Prefix
6
+ from nautobot.ipam.tables import PrefixTable
7
+
8
+
9
+ class PrefixTableTestCase(TestCase):
10
+ def _validate_sorted_queryset_same_with_table_queryset(self, queryset, table_class, field_name):
11
+ with self.subTest(f"Assert sorting {table_class.__name__} on '{field_name}'"):
12
+ table = table_class(queryset, order_by=field_name)
13
+ table_queryset_data = table.data.data.values_list("pk", flat=True)
14
+ sorted_queryset = queryset.order_by(field_name).values_list("pk", flat=True)
15
+ self.assertEqual(list(table_queryset_data), list(sorted_queryset))
16
+
17
+ def test_prefix_table_sort(self):
18
+ """Assert TreeNode model table are orderable."""
19
+ # Due to MySQL's lack of support for combining 'LIMIT' and 'ORDER BY' in a single query,
20
+ # hence this approach.
21
+ pk_list = Prefix.objects.all().values_list("pk", flat=True)[:20]
22
+ pk_list = [str(pk) for pk in pk_list]
23
+ queryset = Prefix.objects.filter(pk__in=pk_list)
24
+
25
+ # Assets model names
26
+ table_avail_fields = ["tenant", "vlan", "namespace"]
27
+ for table_field_name in table_avail_fields:
28
+ self._validate_sorted_queryset_same_with_table_queryset(queryset, PrefixTable, table_field_name)
29
+ self._validate_sorted_queryset_same_with_table_queryset(queryset, PrefixTable, f"-{table_field_name}")
30
+
31
+ # Assert `prefix`
32
+ table_queryset_data = PrefixTable(queryset, order_by="prefix").data.data.values_list("pk", flat=True)
33
+ prefix_queryset = queryset.order_by("network", "prefix_length").values_list("pk", flat=True)
34
+ self.assertEqual(list(table_queryset_data), list(prefix_queryset))
35
+ table_queryset_data = PrefixTable(queryset, order_by="-prefix").data.data.values_list("pk", flat=True)
36
+ prefix_queryset = queryset.order_by("-network", "-prefix_length").values_list("pk", flat=True)
37
+ self.assertEqual(list(table_queryset_data), list(prefix_queryset))
38
+
39
+ # Assets `location_count`
40
+ location_count_queryset = queryset.annotate(location_count=count_related(Location, "prefixes")).all()
41
+ self._validate_sorted_queryset_same_with_table_queryset(location_count_queryset, PrefixTable, "location_count")
42
+ self._validate_sorted_queryset_same_with_table_queryset(location_count_queryset, PrefixTable, "-location_count")
@@ -126,6 +126,7 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
126
126
 
127
127
  class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase, ViewTestCases.ListObjectsViewTestCase):
128
128
  model = Prefix
129
+ filter_on_field = "prefix_length"
129
130
 
130
131
  @classmethod
131
132
  def setUpTestData(cls):
nautobot/ipam/views.py CHANGED
@@ -793,7 +793,7 @@ class IPAddressEditView(generic.ObjectEditView):
793
793
  _, error_msg = retrieve_interface_or_vminterface_from_request(request)
794
794
  if error_msg:
795
795
  messages.warning(request, error_msg)
796
- return redirect(self.get_return_url(request), default_return_url="ipam:ipaddress_add")
796
+ return redirect(self.get_return_url(request, default_return_url="ipam:ipaddress_add"))
797
797
 
798
798
  return super().dispatch(request, *args, **kwargs)
799
799
 
@@ -876,17 +876,17 @@ class IPAddressAssignView(view_mixins.GetReturnURLMixin, generic.ObjectView):
876
876
  """
877
877
 
878
878
  queryset = IPAddress.objects.all()
879
- default_return_url = "ipam:ipaddress_add"
880
879
 
881
880
  def dispatch(self, request, *args, **kwargs):
882
- # Redirect user if an interface has not been provided
883
- if "interface" not in request.GET and "vminterface" not in request.GET:
884
- return redirect(self.get_return_url(request))
881
+ if request.user.is_authenticated:
882
+ # Redirect user if an interface has not been provided
883
+ if "interface" not in request.GET and "vminterface" not in request.GET:
884
+ return redirect(self.get_return_url(request, default_return_url="ipam:ipaddress_add"))
885
885
 
886
- _, error_msg = retrieve_interface_or_vminterface_from_request(request)
887
- if error_msg:
888
- messages.warning(request, error_msg)
889
- return redirect(self.get_return_url(request))
886
+ _, error_msg = retrieve_interface_or_vminterface_from_request(request)
887
+ if error_msg:
888
+ messages.warning(request, error_msg)
889
+ return redirect(self.get_return_url(request, default_return_url="ipam:ipaddress_add"))
890
890
 
891
891
  return super().dispatch(request, *args, **kwargs)
892
892
 
@@ -27,6 +27,7 @@ body {
27
27
  transition-duration: var(--navbar-transition-duration);
28
28
  transition-timing-function: ease-in-out;
29
29
  transition-property: margin-left, width;
30
+ min-height: calc(100vh - 20px);
30
31
  }
31
32
  #main-content > .form {
32
33
  margin-top: 20px;