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
@@ -40,8 +40,8 @@ from nautobot.dcim.choices import (
40
40
  )
41
41
  from nautobot.dcim.filters import (
42
42
  ConsoleConnectionFilterSet,
43
- ControllerDeviceGroupFilterSet,
44
43
  ControllerFilterSet,
44
+ ControllerManagedDeviceGroupFilterSet,
45
45
  InterfaceConnectionFilterSet,
46
46
  PowerConnectionFilterSet,
47
47
  SoftwareImageFileFilterSet,
@@ -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,
@@ -136,6 +136,7 @@ def create_test_device(name):
136
136
 
137
137
  class LocationTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
138
138
  model = LocationType
139
+ sort_on_field = "nestable"
139
140
 
140
141
  @classmethod
141
142
  def setUpTestData(cls):
@@ -254,15 +255,20 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
254
255
 
255
256
  class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
256
257
  model = RackGroup
258
+ sort_on_field = "name"
257
259
 
258
260
  @classmethod
259
261
  def setUpTestData(cls):
260
262
  location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
261
263
 
262
- RackGroup.objects.create(name="Rack Group 1", location=location)
263
- RackGroup.objects.create(name="Rack Group 2", location=location)
264
- RackGroup.objects.create(name="Rack Group 3", location=location)
265
- RackGroup.objects.create(name="Rack Group 8", location=location)
264
+ rack_groups = (
265
+ RackGroup.objects.create(name="Rack Group 1", location=location),
266
+ RackGroup.objects.create(name="Rack Group 2", location=location),
267
+ RackGroup.objects.create(name="Rack Group 3", location=location),
268
+ RackGroup.objects.create(name="Rack Group 8", location=location),
269
+ )
270
+ RackGroup.objects.create(name="Rack Group Child 1", location=location, parent=rack_groups[0])
271
+ RackGroup.objects.create(name="Rack Group Child 2", location=location, parent=rack_groups[0])
266
272
 
267
273
  cls.form_data = {
268
274
  "name": "Rack Group X",
@@ -567,6 +573,7 @@ class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
567
573
  # FIXME(jathan): This has to be replaced with# `get_deletable_object` and
568
574
  # `get_deletable_object_pks` but this is a workaround just so all of these objects are
569
575
  # deletable for now.
576
+ Controller.objects.filter(controller_device__isnull=False).delete()
570
577
  Device.objects.all().delete()
571
578
  DeviceType.objects.all().delete()
572
579
  Platform.objects.all().delete()
@@ -593,6 +600,7 @@ class DeviceTypeTestCase(
593
600
 
594
601
  @classmethod
595
602
  def setUpTestData(cls):
603
+ Controller.objects.filter(controller_device__isnull=False).delete()
596
604
  Device.objects.all().delete()
597
605
  manufacturers = Manufacturer.objects.all()[:2]
598
606
 
@@ -1278,6 +1286,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1278
1286
 
1279
1287
  @classmethod
1280
1288
  def setUpTestData(cls):
1289
+ Controller.objects.filter(controller_device__isnull=False).delete()
1281
1290
  Device.objects.all().delete()
1282
1291
  locations = Location.objects.filter(location_type=LocationType.objects.get(name="Campus"))[:2]
1283
1292
 
@@ -1445,7 +1454,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
1445
1454
  "face": DeviceFaceChoices.FACE_FRONT,
1446
1455
  "secrets_group": secrets_groups[1].pk,
1447
1456
  "software_version": software_versions[1].pk,
1448
- "controller_device_group": ControllerDeviceGroup.objects.first().pk,
1457
+ "controller_managed_device_group": ControllerManagedDeviceGroup.objects.first().pk,
1449
1458
  }
1450
1459
 
1451
1460
  @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
@@ -2227,6 +2236,9 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
2227
2236
  "software_version": software_versions[2].pk,
2228
2237
  }
2229
2238
 
2239
+ def test_table_with_indentation_is_removed_on_filter_or_sort(self):
2240
+ self.skipTest("InventoryItem table has no implementation of indentation.")
2241
+
2230
2242
 
2231
2243
  # TODO: Change base class to PrimaryObjectViewTestCase
2232
2244
  # Blocked by lack of common creation view for cables (termination A must be initialized)
@@ -3119,7 +3131,7 @@ class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3119
3131
  tenant = Tenant.objects.first()
3120
3132
 
3121
3133
  cls.form_data = {
3122
- "deployed_controller_device": device.pk,
3134
+ "controller_device": device.pk,
3123
3135
  "description": "Controller 1 description",
3124
3136
  "external_integration": external_integration.pk,
3125
3137
  "location": location.pk,
@@ -3140,16 +3152,16 @@ class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3140
3152
  }
3141
3153
 
3142
3154
 
3143
- class ControllerDeviceGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3144
- model = ControllerDeviceGroup
3145
- filterset = ControllerDeviceGroupFilterSet
3155
+ class ControllerManagedDeviceGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
3156
+ model = ControllerManagedDeviceGroup
3157
+ filterset = ControllerManagedDeviceGroupFilterSet
3146
3158
 
3147
3159
  @classmethod
3148
3160
  def setUpTestData(cls):
3149
3161
  controllers = Controller.objects.all()
3150
3162
 
3151
3163
  cls.form_data = {
3152
- "name": "Controller Device Group 10",
3164
+ "name": "Managed Device Group 10",
3153
3165
  "controller": controllers[0].pk,
3154
3166
  "weight": 100,
3155
3167
  "devices": [item.pk for item in Device.objects.all()[:2]],
nautobot/dcim/urls.py CHANGED
@@ -40,7 +40,7 @@ router.register("interface-redundancy-groups-associations", views.InterfaceRedun
40
40
  router.register("software-image-files", views.SoftwareImageFileUIViewSet)
41
41
  router.register("software-versions", views.SoftwareVersionUIViewSet)
42
42
  router.register("controllers", views.ControllerUIViewSet)
43
- router.register("controller-device-groups", views.ControllerDeviceGroupUIViewSet)
43
+ router.register("controller-managed-device-groups", views.ControllerManagedDeviceGroupUIViewSet)
44
44
 
45
45
  urlpatterns = [
46
46
  # Location types
nautobot/dcim/views.py CHANGED
@@ -11,7 +11,7 @@ from django.forms import (
11
11
  ModelMultipleChoiceField,
12
12
  MultipleHiddenInput,
13
13
  )
14
- from django.shortcuts import get_object_or_404, redirect, render
14
+ from django.shortcuts import get_object_or_404, HttpResponse, redirect, render
15
15
  from django.utils.functional import cached_property
16
16
  from django.utils.html import format_html
17
17
  from django.views.generic import View
@@ -48,7 +48,7 @@ from .models import (
48
48
  ConsoleServerPort,
49
49
  ConsoleServerPortTemplate,
50
50
  Controller,
51
- ControllerDeviceGroup,
51
+ ControllerManagedDeviceGroup,
52
52
  Device,
53
53
  DeviceBay,
54
54
  DeviceBayTemplate,
@@ -1459,7 +1459,7 @@ class DeviceBulkEditView(generic.BulkEditView):
1459
1459
  "device_type__manufacturer",
1460
1460
  "secrets_group",
1461
1461
  "device_redundancy_group",
1462
- "controller_device_group",
1462
+ "controller_managed_device_group",
1463
1463
  )
1464
1464
  filterset = filters.DeviceFilterSet
1465
1465
  table = tables.DeviceTable
@@ -2359,7 +2359,7 @@ class CableCreateView(generic.ObjectEditView):
2359
2359
  "rear-port": forms.ConnectCableToRearPortForm,
2360
2360
  "power-feed": forms.ConnectCableToPowerFeedForm,
2361
2361
  "circuit-termination": forms.ConnectCableToCircuitTerminationForm,
2362
- }[kwargs.get("termination_b_type")]
2362
+ }.get(kwargs.get("termination_b_type"), None)
2363
2363
 
2364
2364
  return super().dispatch(request, *args, **kwargs)
2365
2365
 
@@ -2376,6 +2376,9 @@ class CableCreateView(generic.ObjectEditView):
2376
2376
  return obj
2377
2377
 
2378
2378
  def get(self, request, *args, **kwargs):
2379
+ if self.model_form is None:
2380
+ return HttpResponse(status_code=400)
2381
+
2379
2382
  obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
2380
2383
 
2381
2384
  # Parse initial data manually to avoid setting field values as lists
@@ -2975,6 +2978,12 @@ class DeviceFamilyUIViewSet(NautobotUIViewSet):
2975
2978
  RequestConfig(request, paginate).configure(device_type_table)
2976
2979
 
2977
2980
  context["device_type_table"] = device_type_table
2981
+
2982
+ total_devices = 0
2983
+ for device_type in device_types:
2984
+ total_devices += device_type.device_count
2985
+ context["total_devices"] = total_devices
2986
+
2978
2987
  return context
2979
2988
 
2980
2989
 
@@ -3021,12 +3030,13 @@ class ControllerUIViewSet(NautobotUIViewSet):
3021
3030
  queryset = Controller.objects.all()
3022
3031
  serializer_class = serializers.ControllerSerializer
3023
3032
  table_class = tables.ControllerTable
3033
+ template_name = "dcim/controller_create.html"
3024
3034
 
3025
3035
  def get_extra_context(self, request, instance):
3026
3036
  context = super().get_extra_context(request, instance)
3027
3037
 
3028
3038
  if self.action == "retrieve" and instance:
3029
- devices = Device.objects.restrict(request.user).filter(controller_device_group__controller=instance)
3039
+ devices = Device.objects.restrict(request.user).filter(controller_managed_device_group__controller=instance)
3030
3040
  devices_table = tables.DeviceTable(devices)
3031
3041
 
3032
3042
  paginate = {
@@ -3040,19 +3050,19 @@ class ControllerUIViewSet(NautobotUIViewSet):
3040
3050
  return context
3041
3051
 
3042
3052
 
3043
- class ControllerDeviceGroupUIViewSet(NautobotUIViewSet):
3044
- filterset_class = filters.ControllerDeviceGroupFilterSet
3045
- filterset_form_class = forms.ControllerDeviceGroupFilterForm
3046
- form_class = forms.ControllerDeviceGroupForm
3047
- bulk_update_form_class = forms.ControllerDeviceGroupBulkEditForm
3053
+ class ControllerManagedDeviceGroupUIViewSet(NautobotUIViewSet):
3054
+ filterset_class = filters.ControllerManagedDeviceGroupFilterSet
3055
+ filterset_form_class = forms.ControllerManagedDeviceGroupFilterForm
3056
+ form_class = forms.ControllerManagedDeviceGroupForm
3057
+ bulk_update_form_class = forms.ControllerManagedDeviceGroupBulkEditForm
3048
3058
  queryset = (
3049
- ControllerDeviceGroup.objects.all()
3059
+ ControllerManagedDeviceGroup.objects.all()
3050
3060
  .prefetch_related("devices")
3051
- .annotate(device_count=count_related(Device, "controller_device_group"))
3061
+ .annotate(device_count=count_related(Device, "controller_managed_device_group"))
3052
3062
  )
3053
- serializer_class = serializers.ControllerDeviceGroupSerializer
3054
- table_class = tables.ControllerDeviceGroupTable
3055
- template_name = "dcim/controllerdevicegroup_create.html"
3063
+ serializer_class = serializers.ControllerManagedDeviceGroupSerializer
3064
+ table_class = tables.ControllerManagedDeviceGroupTable
3065
+ template_name = "dcim/controllermanageddevicegroup_create.html"
3056
3066
 
3057
3067
  def get_extra_context(self, request, instance):
3058
3068
  context = super().get_extra_context(request, instance)
@@ -193,10 +193,18 @@ class ContactSerializer(NautobotModelSerializer):
193
193
  class Meta:
194
194
  model = Contact
195
195
  fields = "__all__"
196
+ # https://www.django-rest-framework.org/api-guide/validators/#optional-fields
197
+ validators = []
198
+ extra_kwargs = {
199
+ "email": {"default": ""},
200
+ "phone": {"default": ""},
201
+ }
196
202
 
197
203
  def validate(self, data):
198
204
  attrs = data.copy()
199
205
  attrs.pop("teams", None)
206
+ validator = UniqueTogetherValidator(queryset=Contact.objects.all(), fields=("name", "phone", "email"))
207
+ validator(attrs, self)
200
208
  super().validate(attrs)
201
209
  return data
202
210
 
@@ -946,7 +954,18 @@ class TeamSerializer(NautobotModelSerializer):
946
954
  class Meta:
947
955
  model = Team
948
956
  fields = "__all__"
949
- extra_kwargs = {"contacts": {"required": False}}
957
+ extra_kwargs = {
958
+ "contacts": {"required": False},
959
+ "email": {"default": ""},
960
+ "phone": {"default": ""},
961
+ }
962
+ # https://www.django-rest-framework.org/api-guide/validators/#optional-fields
963
+ validators = []
964
+
965
+ def validate(self, data):
966
+ validator = UniqueTogetherValidator(queryset=Team.objects.all(), fields=("name", "phone", "email"))
967
+ validator(data, self)
968
+ return super().validate(data)
950
969
 
951
970
 
952
971
  #
@@ -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.ExtrasRootView
5
+ router = OrderedDefaultRouter(view_name="Extras")
7
6
 
8
7
  # Computed Fields
9
8
  router.register("computed-fields", views.ComputedFieldViewSet)
@@ -16,7 +16,6 @@ from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, Valida
16
16
  from rest_framework.parsers import JSONParser, MultiPartParser
17
17
  from rest_framework.permissions import IsAuthenticated
18
18
  from rest_framework.response import Response
19
- from rest_framework.routers import APIRootView
20
19
 
21
20
  from nautobot.core.api.authentication import TokenPermissions
22
21
  from nautobot.core.api.utils import get_serializer_for_model
@@ -77,15 +76,6 @@ from nautobot.extras.utils import get_worker_count
77
76
  from . import serializers
78
77
 
79
78
 
80
- class ExtrasRootView(APIRootView):
81
- """
82
- Extras API root view
83
- """
84
-
85
- def get_view_name(self):
86
- return "Extras"
87
-
88
-
89
79
  class NotesViewSetMixin:
90
80
  def restrict_queryset(self, request, *args, **kwargs):
91
81
  """
nautobot/extras/apps.py CHANGED
@@ -12,6 +12,13 @@ logger = logging.getLogger(__name__)
12
12
 
13
13
  class ExtrasConfig(NautobotConfig):
14
14
  name = "nautobot.extras"
15
+ searchable_models = [
16
+ "contact",
17
+ "dynamicgroup",
18
+ "externalintegration",
19
+ "gitrepository",
20
+ "team",
21
+ ]
15
22
 
16
23
  def ready(self):
17
24
  super().ready()
@@ -3,11 +3,13 @@ import uuid
3
3
 
4
4
  from django.contrib.auth import get_user_model
5
5
  from django.contrib.auth.models import AnonymousUser
6
+ from django.db import transaction
6
7
  from django.test.client import RequestFactory
7
8
 
8
9
  from nautobot.extras.choices import ObjectChangeEventContextChoices
10
+ from nautobot.extras.constants import CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL
9
11
  from nautobot.extras.models import ObjectChange
10
- from nautobot.extras.signals import change_context_state
12
+ from nautobot.extras.signals import change_context_state, get_user_if_authenticated
11
13
  from nautobot.extras.webhooks import enqueue_webhooks
12
14
 
13
15
 
@@ -25,9 +27,12 @@ class ChangeContext:
25
27
  :param change_id: Optional uuid object to uniquely identify the transaction. One will be generated if not supplied
26
28
  """
27
29
 
30
+ defer_object_changes = False # advanced usage, for creating object changes in bulk
31
+
28
32
  def __init__(self, user=None, request=None, context=None, context_detail="", change_id=None):
29
33
  self.request = request
30
34
  self.user = user
35
+ self.reset_deferred_object_changes()
31
36
 
32
37
  if self.request is None and self.user is None:
33
38
  raise TypeError("Either user or request must be provided")
@@ -46,11 +51,52 @@ class ChangeContext:
46
51
  if self.change_id is None:
47
52
  self.change_id = uuid.uuid4()
48
53
 
49
- def get_user(self):
54
+ def get_user(self, instance=None):
50
55
  """Return self.user if set, otherwise return self.request.user"""
51
56
  if self.user is not None:
52
- return self.user
53
- return self.request.user
57
+ return get_user_if_authenticated(self.user, instance)
58
+ return get_user_if_authenticated(self.request.user, instance)
59
+
60
+ def as_dict(self, instance=None):
61
+ """
62
+ Return ChangeContext attributes in dictionary format
63
+ """
64
+ context = {
65
+ "user": self.get_user(instance),
66
+ "change_id": self.change_id,
67
+ "context": self.context,
68
+ }
69
+ return context
70
+
71
+ def _object_change_batch(self, n):
72
+ # Return first n keys from the self.deferred_object_changes dict
73
+ keys = []
74
+ for i, k in enumerate(self.deferred_object_changes.keys()):
75
+ if i >= n:
76
+ return keys
77
+ keys.append(k)
78
+ return keys
79
+
80
+ def reset_deferred_object_changes(self):
81
+ self.deferred_object_changes = {}
82
+
83
+ def flush_deferred_object_changes(self, batch_size=1000):
84
+ if self.defer_object_changes:
85
+ self.create_object_changes(batch_size=batch_size)
86
+
87
+ def create_object_changes(self, batch_size=1000):
88
+ while self.deferred_object_changes:
89
+ create_object_changes = []
90
+ for key in self._object_change_batch(batch_size):
91
+ for entry in self.deferred_object_changes[key]:
92
+ objectchange = entry["instance"].to_objectchange(entry["action"])
93
+ objectchange.user = entry["user"]
94
+ objectchange.request_id = self.change_id
95
+ objectchange.change_context = self.context
96
+ objectchange.change_context_detail = self.context_detail[:CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL]
97
+ create_object_changes.append(objectchange)
98
+ self.deferred_object_changes.pop(key, None)
99
+ ObjectChange.objects.bulk_create(create_object_changes, batch_size=batch_size)
54
100
 
55
101
 
56
102
  class JobChangeContext(ChangeContext):
@@ -152,3 +198,24 @@ def web_request_context(
152
198
  for object_change in ObjectChange.objects.filter(request_id=change_context.change_id).iterator():
153
199
  enqueue_job_hooks(object_change)
154
200
  enqueue_webhooks(object_change)
201
+
202
+
203
+ @contextmanager
204
+ def deferred_change_logging_for_bulk_operation():
205
+ """
206
+ Defers change logging until the end of the context manager to improve performance. For use with bulk edit views. This
207
+ context manager is wrapped in an atomic transaction.
208
+ """
209
+
210
+ change_context = change_context_state.get()
211
+ if change_context is None:
212
+ raise ValueError("Change logging must be enabled before using deferred_change_logging_for_bulk_operation")
213
+
214
+ with transaction.atomic():
215
+ try:
216
+ change_context.defer_object_changes = True
217
+ yield
218
+ change_context.flush_deferred_object_changes()
219
+ finally:
220
+ change_context.defer_object_changes = False
221
+ change_context.reset_deferred_object_changes()
@@ -1,7 +1,11 @@
1
+ from difflib import get_close_matches
2
+
1
3
  from django.conf import settings
2
4
  from django.contrib.auth import get_user_model
3
5
  from django.contrib.contenttypes.models import ContentType
6
+ from django.db.models import Q
4
7
  import django_filters
8
+ from drf_spectacular.utils import extend_schema_field
5
9
 
6
10
  from nautobot.core.api.exceptions import SerializerNotFound
7
11
  from nautobot.core.api.utils import get_serializer_for_model
@@ -447,7 +451,54 @@ class NautobotFilterSet(
447
451
  #
448
452
 
449
453
 
450
- class ContactFilterSet(NameSearchFilterSet, NautobotFilterSet):
454
+ class ContactTeamFilterSet(NameSearchFilterSet, NautobotFilterSet):
455
+ """Base filter set for Contacts and Teams."""
456
+
457
+ similar_to_location_data = NaturalKeyOrPKMultipleChoiceFilter(
458
+ queryset=Location.objects.all(),
459
+ label="Similar to location contact data",
460
+ method="_similar_to_location_data",
461
+ )
462
+
463
+ def generate_query__similar_to_location_data(self, queryset, locations):
464
+ """Helper method used by _similar_to_location_data() method."""
465
+ query_params = Q()
466
+ for location in locations:
467
+ contact_name = location.contact_name
468
+ contact_phone = location.contact_phone
469
+ contact_email = location.contact_email
470
+ if contact_name:
471
+ contact_names = list(queryset.order_by().values_list("name", flat=True).distinct())
472
+ name_matches = get_close_matches(contact_name, contact_names, cutoff=0.8)
473
+ if name_matches:
474
+ query_params |= Q(name__in=name_matches)
475
+ if contact_phone:
476
+ contact_phones = list(queryset.order_by().values_list("phone", flat=True).distinct())
477
+ phone_matches = get_close_matches(contact_phone, contact_phones, cutoff=0.8)
478
+ if phone_matches:
479
+ query_params |= Q(phone__in=phone_matches)
480
+ if contact_email:
481
+ contact_emails = list(queryset.order_by().values_list("email", flat=True).distinct())
482
+ # fuzzy matching for emails doesn't make sense, use case insensitive match here
483
+ email_matches = [e for e in contact_emails if e.casefold() == contact_email.casefold()]
484
+ if email_matches:
485
+ query_params |= Q(email__in=email_matches)
486
+
487
+ return query_params
488
+
489
+ @extend_schema_field({"type": "string"})
490
+ def _similar_to_location_data(self, queryset, name, value):
491
+ """FilterSet method for getting Contacts or Teams that are similar to the explicit contact fields of a location"""
492
+ if value:
493
+ params = self.generate_query__similar_to_location_data(queryset, value)
494
+ if len(params) > 0:
495
+ return queryset.filter(params)
496
+ else:
497
+ return queryset.none()
498
+ return queryset
499
+
500
+
501
+ class ContactFilterSet(ContactTeamFilterSet):
451
502
  class Meta:
452
503
  model = Contact
453
504
  fields = "__all__"
@@ -1109,7 +1160,7 @@ class TagFilterSet(NautobotFilterSet):
1109
1160
  #
1110
1161
 
1111
1162
 
1112
- class TeamFilterSet(NameSearchFilterSet, NautobotFilterSet):
1163
+ class TeamFilterSet(ContactTeamFilterSet):
1113
1164
  class Meta:
1114
1165
  model = Team
1115
1166
  fields = "__all__"
@@ -8,8 +8,9 @@ from nautobot.core.filters import (
8
8
  MultiValueNumberFilter,
9
9
  )
10
10
  from nautobot.core.forms import NullableDateField
11
- from nautobot.core.forms.widgets import StaticSelect2Multiple
11
+ from nautobot.core.utils.data import is_uuid
12
12
  from nautobot.extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
13
+ from nautobot.extras.models import CustomFieldChoice
13
14
 
14
15
  EXACT_FILTER_TYPES = (
15
16
  CustomFieldTypeChoices.TYPE_BOOLEAN,
@@ -71,19 +72,23 @@ class CustomFieldJSONFilter(CustomFieldFilterMixin, django_filters.Filter):
71
72
  """Custom field single value filter for backwards compatibility"""
72
73
 
73
74
 
74
- class CustomFieldMultiSelectFilter(CustomFieldFilterMixin, MultiValueCharFilter):
75
- """This provides functionality for filtering custom fields with multiple select type"""
75
+ class CustomFieldSelectFilter(CustomFieldFilterMixin, MultiValueCharFilter):
76
+ """Filter for custom fields of type TYPE_SELECT."""
76
77
 
77
- def __init__(self, *args, **kwargs):
78
- kwargs.setdefault("lookup_expr", "contains")
79
- super().__init__(*args, **kwargs)
78
+ def get_filter_predicate(self, v):
79
+ if is_uuid(v):
80
+ try:
81
+ v = self.custom_field.custom_field_choices.get(pk=v).value
82
+ except CustomFieldChoice.DoesNotExist:
83
+ v = ""
84
+ return super().get_filter_predicate(v)
80
85
 
81
86
 
82
- class CustomFieldMultiValueSelectFilter(CustomFieldFilterMixin, django_filters.MultipleChoiceFilter):
83
- """This provides functionality for filtering custom fields with select type"""
87
+ class CustomFieldMultiSelectFilter(CustomFieldSelectFilter):
88
+ """Filter for custom fields of type TYPE_MULTISELECT."""
84
89
 
85
90
  def __init__(self, *args, **kwargs):
86
- self.field_class.widget = StaticSelect2Multiple
91
+ kwargs.setdefault("lookup_expr", "contains")
87
92
  super().__init__(*args, **kwargs)
88
93
 
89
94
 
@@ -27,6 +27,7 @@ from nautobot.extras.filters.customfields import (
27
27
  CustomFieldMultiValueDateFilter,
28
28
  CustomFieldMultiValueNumberFilter,
29
29
  CustomFieldNumberFilter,
30
+ CustomFieldSelectFilter,
30
31
  )
31
32
  from nautobot.extras.models import (
32
33
  ConfigContextSchema,
@@ -61,12 +62,16 @@ class CustomFieldModelFilterSetMixin(django_filters.FilterSet):
61
62
  super().__init__(*args, **kwargs)
62
63
 
63
64
  custom_field_filter_classes = {
65
+ # Here, for the "base" filters for each custom field, for backwards compatibility, use single-value filters.
66
+ # For the "extended" filters, see below, we use multi-value filters.
67
+ # 3.0 TODO: switch the "base" filters to multi-value filters as well.
64
68
  CustomFieldTypeChoices.TYPE_DATE: CustomFieldDateFilter,
65
69
  CustomFieldTypeChoices.TYPE_BOOLEAN: CustomFieldBooleanFilter,
66
70
  CustomFieldTypeChoices.TYPE_INTEGER: CustomFieldNumberFilter,
67
71
  CustomFieldTypeChoices.TYPE_JSON: CustomFieldJSONFilter,
72
+ # The below are multi-value filters already:
68
73
  CustomFieldTypeChoices.TYPE_MULTISELECT: CustomFieldMultiSelectFilter,
69
- CustomFieldTypeChoices.TYPE_SELECT: CustomFieldMultiSelectFilter,
74
+ CustomFieldTypeChoices.TYPE_SELECT: CustomFieldSelectFilter,
70
75
  }
71
76
 
72
77
  custom_fields = CustomField.objects.get_for_model(self._meta.model, exclude_filter_disabled=True)
@@ -28,6 +28,13 @@ class ContactForm(NautobotModelForm):
28
28
  "tags",
29
29
  ]
30
30
 
31
+ def __init__(self, instance=None, initial=None, **kwargs):
32
+ if instance is not None:
33
+ if initial is None:
34
+ initial = {}
35
+ initial.setdefault("teams", instance.teams.all())
36
+ super().__init__(instance=instance, initial=initial, **kwargs)
37
+
31
38
  def save(self, *args, **kwargs):
32
39
  """
33
40
  Since `teams` field on Contact Model is the reverse side of an M2M,
@@ -1,4 +1,5 @@
1
1
  """Nautobot custom health checks."""
2
+
2
3
  from typing import Optional
3
4
  from urllib.parse import urlparse
4
5
 
nautobot/extras/jobs.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Jobs functionality - consolidates and replaces legacy "custom scripts" and "reports" features."""
2
+
2
3
  from collections import OrderedDict
3
4
  import functools
4
5
  import inspect
@@ -1,6 +1,5 @@
1
1
  from celery import states
2
2
  from django.utils import timezone
3
- from django_celery_beat.managers import ExtendedManager
4
3
  from django_celery_results.managers import TaskResultManager, transaction_retry
5
4
 
6
5
  from nautobot.core.models import BaseManager
@@ -8,6 +7,20 @@ from nautobot.core.models.querysets import RestrictedQuerySet
8
7
 
9
8
 
10
9
  class JobResultManager(BaseManager.from_queryset(RestrictedQuerySet), TaskResultManager):
10
+ def get_task(self, task_id):
11
+ """Get result for task by ``task_id``.
12
+
13
+ This overloads `TaskResultManager.get_task` provided by `django-celery-results` to manage custom
14
+ behaviors for integration with Nautobot.
15
+ """
16
+ try:
17
+ return self.get(id=task_id)
18
+ except self.model.DoesNotExist:
19
+ if self._last_id == task_id:
20
+ self.warn_if_repeatable_read()
21
+ self._last_id = task_id
22
+ return self.model(id=task_id)
23
+
11
24
  @transaction_retry(max_retries=2)
12
25
  def store_result(
13
26
  self,
@@ -122,5 +135,5 @@ class JobResultManager(BaseManager.from_queryset(RestrictedQuerySet), TaskResult
122
135
  return obj
123
136
 
124
137
 
125
- class ScheduledJobsManager(BaseManager.from_queryset(RestrictedQuerySet), ExtendedManager):
138
+ class ScheduledJobsManager(BaseManager.from_queryset(RestrictedQuerySet)):
126
139
  pass
@@ -19,6 +19,7 @@ class ContactTeamSharedBase(PrimaryModel):
19
19
  address = models.TextField(blank=True)
20
20
 
21
21
  comments = models.TextField(blank=True)
22
+ is_contact_associable_model = False
22
23
 
23
24
  class Meta:
24
25
  abstract = True