nautobot 2.2.2__py3-none-any.whl → 2.2.4__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 (362) hide show
  1. nautobot/apps/jobs.py +2 -0
  2. nautobot/core/api/utils.py +12 -9
  3. nautobot/core/apps/__init__.py +2 -2
  4. nautobot/core/celery/__init__.py +79 -68
  5. nautobot/core/celery/backends.py +9 -1
  6. nautobot/core/celery/control.py +4 -7
  7. nautobot/core/celery/schedulers.py +4 -2
  8. nautobot/core/celery/task.py +78 -5
  9. nautobot/core/graphql/schema.py +2 -1
  10. nautobot/core/jobs/__init__.py +2 -1
  11. nautobot/core/settings.py +6 -4
  12. nautobot/core/settings.yaml +51 -16
  13. nautobot/core/templates/admin/base.html +2 -2
  14. nautobot/core/templates/base_django.html +2 -2
  15. nautobot/core/templates/buttons/export.html +47 -47
  16. nautobot/core/templates/generic/object_list.html +3 -3
  17. nautobot/core/templates/inc/javascript.html +3 -0
  18. nautobot/core/templates/inc/media.html +3 -0
  19. nautobot/core/templates/login.html +2 -2
  20. nautobot/core/templates/nautobot_config.py.j2 +2 -0
  21. nautobot/core/templatetags/helpers.py +66 -9
  22. nautobot/core/testing/__init__.py +6 -1
  23. nautobot/core/testing/api.py +12 -13
  24. nautobot/core/testing/mixins.py +2 -2
  25. nautobot/core/testing/views.py +50 -51
  26. nautobot/core/tests/test_api.py +23 -2
  27. nautobot/core/tests/test_jobs.py +79 -2
  28. nautobot/core/tests/test_templatetags_helpers.py +32 -0
  29. nautobot/core/tests/test_views.py +52 -0
  30. nautobot/core/tests/test_views_utils.py +22 -1
  31. nautobot/core/utils/module_loading.py +89 -0
  32. nautobot/core/views/mixins.py +4 -0
  33. nautobot/core/views/utils.py +3 -2
  34. nautobot/dcim/choices.py +14 -0
  35. nautobot/dcim/forms.py +51 -1
  36. nautobot/dcim/models/device_components.py +9 -5
  37. nautobot/dcim/templates/dcim/location.html +32 -13
  38. nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +102 -0
  39. nautobot/dcim/tests/test_views.py +376 -55
  40. nautobot/dcim/urls.py +5 -0
  41. nautobot/dcim/views.py +172 -21
  42. nautobot/extras/api/serializers.py +17 -6
  43. nautobot/extras/api/views.py +21 -10
  44. nautobot/extras/constants.py +3 -3
  45. nautobot/extras/datasources/git.py +47 -58
  46. nautobot/extras/forms/forms.py +3 -1
  47. nautobot/extras/jobs.py +79 -146
  48. nautobot/extras/models/datasources.py +0 -2
  49. nautobot/extras/models/jobs.py +36 -18
  50. nautobot/extras/plugins/__init__.py +1 -20
  51. nautobot/extras/signals.py +6 -9
  52. nautobot/extras/test_jobs/__init__.py +8 -0
  53. nautobot/extras/test_jobs/dry_run.py +3 -2
  54. nautobot/extras/test_jobs/fail.py +43 -0
  55. nautobot/extras/test_jobs/ipaddress_vars.py +40 -1
  56. nautobot/extras/test_jobs/jobs_module/__init__.py +5 -0
  57. nautobot/extras/test_jobs/jobs_module/jobs_submodule/__init__.py +1 -0
  58. nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +6 -0
  59. nautobot/extras/test_jobs/pass.py +40 -0
  60. nautobot/extras/test_jobs/relative_import.py +11 -0
  61. nautobot/extras/tests/test_api.py +3 -0
  62. nautobot/extras/tests/test_context_managers.py +18 -0
  63. nautobot/extras/tests/test_datasources.py +125 -118
  64. nautobot/extras/tests/test_job_variables.py +57 -15
  65. nautobot/extras/tests/test_jobs.py +135 -1
  66. nautobot/extras/tests/test_models.py +26 -19
  67. nautobot/extras/tests/test_plugins.py +1 -3
  68. nautobot/extras/tests/test_views.py +2 -4
  69. nautobot/extras/utils.py +2 -1
  70. nautobot/extras/views.py +82 -116
  71. nautobot/ipam/api/views.py +8 -1
  72. nautobot/ipam/graphql/types.py +11 -0
  73. nautobot/ipam/mixins.py +32 -0
  74. nautobot/ipam/models.py +2 -1
  75. nautobot/ipam/querysets.py +6 -1
  76. nautobot/ipam/tests/test_models.py +82 -0
  77. nautobot/ipam/views.py +6 -6
  78. nautobot/project-static/docs/404.html +107 -51
  79. nautobot/project-static/docs/apps/index.html +107 -51
  80. nautobot/project-static/docs/apps/nautobot-apps.html +107 -51
  81. nautobot/project-static/docs/assets/_mkdocstrings.css +6 -1
  82. nautobot/project-static/docs/assets/extra.css +11 -0
  83. nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js +29 -0
  84. nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js.map +7 -0
  85. nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css +1 -0
  86. nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css.map +1 -0
  87. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +107 -51
  88. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +107 -51
  89. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +108 -52
  90. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +107 -51
  91. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +107 -51
  92. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +107 -51
  93. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +107 -51
  94. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +107 -51
  95. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +107 -51
  96. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +107 -51
  97. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +107 -51
  98. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +107 -51
  99. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +107 -51
  100. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +287 -262
  101. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +107 -51
  102. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +107 -51
  103. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +107 -51
  104. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +107 -51
  105. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +107 -51
  106. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +107 -51
  107. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +107 -51
  108. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +107 -51
  109. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +107 -51
  110. nautobot/project-static/docs/development/apps/api/configuration-view.html +110 -54
  111. nautobot/project-static/docs/development/apps/api/database-backend-config.html +110 -54
  112. nautobot/project-static/docs/development/apps/api/models/django-admin.html +107 -51
  113. nautobot/project-static/docs/development/apps/api/models/global-search.html +110 -54
  114. nautobot/project-static/docs/development/apps/api/models/graphql.html +113 -57
  115. nautobot/project-static/docs/development/apps/api/models/index.html +107 -51
  116. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +113 -57
  117. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +107 -51
  118. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +110 -54
  119. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +107 -51
  120. nautobot/project-static/docs/development/apps/api/platform-features/index.html +107 -51
  121. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +110 -54
  122. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +111 -55
  123. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +110 -54
  124. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +110 -54
  125. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +107 -51
  126. nautobot/project-static/docs/development/apps/api/prometheus.html +110 -54
  127. nautobot/project-static/docs/development/apps/api/setup.html +107 -51
  128. nautobot/project-static/docs/development/apps/api/testing.html +113 -57
  129. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +110 -54
  130. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +110 -54
  131. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +107 -51
  132. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +107 -51
  133. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +113 -57
  134. nautobot/project-static/docs/development/apps/api/views/base-template.html +107 -51
  135. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +110 -54
  136. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +107 -51
  137. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +110 -54
  138. nautobot/project-static/docs/development/apps/api/views/index.html +107 -51
  139. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +113 -57
  140. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +122 -66
  141. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +110 -54
  142. nautobot/project-static/docs/development/apps/api/views/notes.html +110 -54
  143. nautobot/project-static/docs/development/apps/api/views/rest-api.html +107 -51
  144. nautobot/project-static/docs/development/apps/api/views/urls.html +107 -51
  145. nautobot/project-static/docs/development/apps/index.html +128 -72
  146. nautobot/project-static/docs/development/apps/migration/code-updates.html +107 -51
  147. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +107 -51
  148. nautobot/project-static/docs/development/apps/migration/from-v1.html +107 -51
  149. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +107 -51
  150. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +107 -51
  151. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +107 -51
  152. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +107 -51
  153. nautobot/project-static/docs/development/apps/porting-from-netbox.html +110 -54
  154. nautobot/project-static/docs/development/core/application-registry.html +242 -144
  155. nautobot/project-static/docs/development/core/best-practices.html +122 -66
  156. nautobot/project-static/docs/development/core/bootstrap-ui.html +107 -51
  157. nautobot/project-static/docs/development/core/caching.html +107 -51
  158. nautobot/project-static/docs/development/core/controllers.html +107 -51
  159. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +113 -57
  160. nautobot/project-static/docs/development/core/generic-views.html +110 -54
  161. nautobot/project-static/docs/development/core/getting-started.html +135 -79
  162. nautobot/project-static/docs/development/core/homepage.html +110 -54
  163. nautobot/project-static/docs/development/core/index.html +107 -51
  164. nautobot/project-static/docs/development/core/model-checklist.html +156 -52
  165. nautobot/project-static/docs/development/core/model-features.html +108 -52
  166. nautobot/project-static/docs/development/core/natural-keys.html +110 -54
  167. nautobot/project-static/docs/development/core/navigation-menu.html +107 -51
  168. nautobot/project-static/docs/development/core/release-checklist.html +107 -51
  169. nautobot/project-static/docs/development/core/role-internals.html +107 -51
  170. nautobot/project-static/docs/development/core/settings.html +107 -51
  171. nautobot/project-static/docs/development/core/style-guide.html +110 -54
  172. nautobot/project-static/docs/development/core/templates.html +113 -57
  173. nautobot/project-static/docs/development/core/testing.html +125 -69
  174. nautobot/project-static/docs/development/core/user-preferences.html +107 -51
  175. nautobot/project-static/docs/development/index.html +107 -51
  176. nautobot/project-static/docs/development/jobs/index.html +504 -172
  177. nautobot/project-static/docs/development/jobs/migration/from-v1.html +111 -55
  178. nautobot/project-static/docs/docker/index.html +3 -3
  179. nautobot/project-static/docs/index.html +125 -69
  180. nautobot/project-static/docs/installation/selinux-troubleshooting.html +3 -3
  181. nautobot/project-static/docs/objects.inv +0 -0
  182. nautobot/project-static/docs/release-notes/index.html +107 -51
  183. nautobot/project-static/docs/release-notes/version-1.0.html +107 -51
  184. nautobot/project-static/docs/release-notes/version-1.1.html +107 -51
  185. nautobot/project-static/docs/release-notes/version-1.2.html +107 -51
  186. nautobot/project-static/docs/release-notes/version-1.3.html +107 -51
  187. nautobot/project-static/docs/release-notes/version-1.4.html +107 -51
  188. nautobot/project-static/docs/release-notes/version-1.5.html +116 -60
  189. nautobot/project-static/docs/release-notes/version-1.6.html +107 -51
  190. nautobot/project-static/docs/release-notes/version-2.0.html +110 -54
  191. nautobot/project-static/docs/release-notes/version-2.1.html +107 -51
  192. nautobot/project-static/docs/release-notes/version-2.2.html +500 -114
  193. nautobot/project-static/docs/requirements.txt +2 -2
  194. nautobot/project-static/docs/search/search_index.json +1 -1
  195. nautobot/project-static/docs/sitemap.xml +262 -262
  196. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  197. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +107 -51
  198. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +107 -51
  199. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +107 -51
  200. nautobot/project-static/docs/user-guide/administration/configuration/index.html +107 -51
  201. nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +251 -164
  202. nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +113 -57
  203. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +107 -51
  204. nautobot/project-static/docs/user-guide/administration/guides/caching.html +113 -57
  205. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +107 -51
  206. nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +107 -51
  207. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +107 -51
  208. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +113 -57
  209. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +107 -51
  210. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +107 -51
  211. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +107 -51
  212. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +171 -112
  213. nautobot/project-static/docs/user-guide/administration/installation/docker.html +13 -8626
  214. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +117 -61
  215. nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +13 -8614
  216. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +252 -165
  217. nautobot/project-static/docs/user-guide/administration/installation/index.html +165 -192
  218. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +411 -691
  219. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +248 -229
  220. nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +13 -8118
  221. nautobot/project-static/docs/user-guide/administration/installation/services.html +350 -240
  222. nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +8684 -0
  223. nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +8672 -0
  224. nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +8176 -0
  225. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +110 -54
  226. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +110 -54
  227. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +155 -99
  228. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +107 -51
  229. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +109 -53
  230. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +107 -51
  231. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +107 -51
  232. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +107 -51
  233. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +107 -51
  234. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +107 -51
  235. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +107 -51
  236. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +117 -58
  237. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +113 -57
  238. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +107 -51
  239. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +107 -51
  240. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +107 -51
  241. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +107 -51
  242. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +110 -54
  243. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +107 -51
  244. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +110 -54
  245. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +110 -54
  246. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +110 -54
  247. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +110 -54
  248. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +107 -51
  249. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +107 -51
  250. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +113 -57
  251. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +110 -54
  252. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +110 -54
  253. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +110 -54
  254. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +110 -54
  255. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +116 -60
  256. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +110 -54
  257. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +110 -54
  258. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +119 -63
  259. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +110 -54
  260. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +110 -54
  261. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +113 -57
  262. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +113 -57
  263. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +113 -57
  264. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +107 -51
  265. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +113 -57
  266. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +107 -51
  267. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +110 -54
  268. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +110 -54
  269. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +107 -51
  270. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +110 -54
  271. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +110 -54
  272. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +107 -51
  273. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +107 -51
  274. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +107 -51
  275. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +110 -54
  276. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +110 -54
  277. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +110 -54
  278. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +110 -54
  279. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +107 -51
  280. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +113 -57
  281. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +110 -54
  282. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +110 -54
  283. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +110 -54
  284. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +125 -69
  285. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +113 -57
  286. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +128 -72
  287. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +110 -54
  288. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +107 -51
  289. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +110 -54
  290. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +227 -60
  291. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +107 -51
  292. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +113 -57
  293. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +107 -51
  294. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +110 -54
  295. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +107 -51
  296. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +107 -51
  297. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +107 -51
  298. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +107 -51
  299. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +113 -57
  300. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +113 -57
  301. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +107 -51
  302. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +113 -57
  303. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +107 -51
  304. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +107 -51
  305. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +107 -51
  306. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +107 -51
  307. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +107 -51
  308. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +107 -51
  309. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +107 -51
  310. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +107 -51
  311. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +107 -51
  312. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +110 -54
  313. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +113 -57
  314. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +107 -51
  315. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +110 -54
  316. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +107 -51
  317. nautobot/project-static/docs/user-guide/index.html +109 -53
  318. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +107 -51
  319. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +113 -57
  320. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +128 -72
  321. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +107 -51
  322. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +125 -69
  323. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +107 -51
  324. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +110 -54
  325. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +125 -69
  326. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +110 -54
  327. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +107 -51
  328. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +107 -51
  329. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +143 -100
  330. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +113 -57
  331. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +113 -57
  332. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +110 -54
  333. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +123 -67
  334. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +107 -51
  335. nautobot/project-static/docs/user-guide/platform-functionality/note.html +110 -54
  336. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +116 -60
  337. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +110 -54
  338. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +131 -75
  339. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +149 -93
  340. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +110 -54
  341. nautobot/project-static/docs/user-guide/platform-functionality/role.html +107 -51
  342. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +116 -60
  343. nautobot/project-static/docs/user-guide/platform-functionality/status.html +119 -63
  344. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +110 -54
  345. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +137 -81
  346. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +110 -54
  347. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +107 -51
  348. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +110 -54
  349. nautobot/project-static/js/forms.js +18 -11
  350. nautobot/tenancy/views.py +2 -6
  351. nautobot/virtualization/views.py +5 -9
  352. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/METADATA +4 -4
  353. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/RECORD +357 -348
  354. nautobot/extras/test_jobs/job_variables.py +0 -93
  355. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js +0 -29
  356. nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js.map +0 -7
  357. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +0 -1
  358. nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +0 -1
  359. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/LICENSE.txt +0 -0
  360. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/NOTICE +0 -0
  361. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/WHEEL +0 -0
  362. {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/entry_points.txt +0 -0
nautobot/dcim/urls.py CHANGED
@@ -84,6 +84,11 @@ urlpatterns = [
84
84
  name="location_notes",
85
85
  kwargs={"model": Location},
86
86
  ),
87
+ path(
88
+ "locations/<uuid:pk>/migrate-data-to-contact/",
89
+ views.MigrateLocationDataToContactView.as_view(),
90
+ name="location_migrate_data_to_contact",
91
+ ),
87
92
  path(
88
93
  "locations/<uuid:object_id>/images/add/",
89
94
  ImageAttachmentEditView.as_view(),
nautobot/dcim/views.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from collections import OrderedDict
2
+ import logging
2
3
  import uuid
3
4
 
4
5
  from django.contrib import messages
5
6
  from django.contrib.contenttypes.models import ContentType
7
+ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
6
8
  from django.core.paginator import EmptyPage, PageNotAnInteger
7
9
  from django.db import transaction
8
10
  from django.db.models import F, Prefetch
@@ -12,15 +14,19 @@ from django.forms import (
12
14
  MultipleHiddenInput,
13
15
  )
14
16
  from django.shortcuts import get_object_or_404, HttpResponse, redirect, render
17
+ from django.utils.encoding import iri_to_uri
15
18
  from django.utils.functional import cached_property
16
19
  from django.utils.html import format_html
20
+ from django.utils.http import url_has_allowed_host_and_scheme
17
21
  from django.views.generic import View
18
22
  from django_tables2 import RequestConfig
19
23
 
20
24
  from nautobot.circuits.models import Circuit
21
- from nautobot.core.forms import ConfirmationForm
25
+ from nautobot.core.forms import ConfirmationForm, restrict_form_fields
22
26
  from nautobot.core.models.querysets import count_related
27
+ from nautobot.core.templatetags.helpers import has_perms
23
28
  from nautobot.core.utils.permissions import get_permission_for_model
29
+ from nautobot.core.utils.requests import normalize_querydict
24
30
  from nautobot.core.views import generic
25
31
  from nautobot.core.views.mixins import (
26
32
  GetReturnURLMixin,
@@ -30,7 +36,10 @@ from nautobot.core.views.mixins import (
30
36
  )
31
37
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
32
38
  from nautobot.core.views.viewsets import NautobotUIViewSet
39
+ from nautobot.dcim.choices import LocationDataToContactActionChoices
40
+ from nautobot.dcim.forms import LocationMigrateDataToContactForm
33
41
  from nautobot.dcim.utils import get_all_network_driver_mappings, get_network_driver_mapping_tool_names
42
+ from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
34
43
  from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
35
44
  from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
36
45
  from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable
@@ -83,6 +92,8 @@ from .models import (
83
92
  VirtualChassis,
84
93
  )
85
94
 
95
+ logger = logging.getLogger(__name__)
96
+
86
97
 
87
98
  class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
88
99
  """
@@ -188,6 +199,7 @@ class LocationTypeView(generic.ObjectView):
188
199
  return {
189
200
  "children_table": children_table,
190
201
  "locations_table": locations_table,
202
+ **super().get_extra_context(request, instance),
191
203
  }
192
204
 
193
205
 
@@ -283,6 +295,10 @@ class LocationView(generic.ObjectView):
283
295
  "children_table": children_table,
284
296
  "rack_groups": rack_groups,
285
297
  "stats": stats,
298
+ "contact_association_permission": ["extras.add_contactassociation"],
299
+ # show the button if any of these fields have non-empty value.
300
+ "show_convert_to_contact_button": instance.contact_name or instance.contact_phone or instance.contact_email,
301
+ **super().get_extra_context(request, instance),
286
302
  }
287
303
 
288
304
 
@@ -314,6 +330,140 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
314
330
  table = tables.LocationTable
315
331
 
316
332
 
333
+ class MigrateLocationDataToContactView(generic.ObjectEditView):
334
+ queryset = Location.objects.all()
335
+ model_form = LocationMigrateDataToContactForm
336
+ template_name = "dcim/location_migrate_data_to_contact.html"
337
+
338
+ def get(self, request, *args, **kwargs):
339
+ obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
340
+
341
+ initial_data = normalize_querydict(request.GET, form_class=self.model_form)
342
+ # remove status from the location itself
343
+ initial_data["status"] = None
344
+ initial_data["location"] = obj.pk
345
+
346
+ # populate contact tab fields initial data
347
+ initial_data["name"] = obj.contact_name
348
+ initial_data["phone"] = obj.contact_phone
349
+ initial_data["email"] = obj.contact_email
350
+ form = self.model_form(instance=obj, initial=initial_data)
351
+ restrict_form_fields(form, request.user)
352
+ return render(
353
+ request,
354
+ self.template_name,
355
+ {
356
+ "obj": obj,
357
+ "obj_type": self.queryset.model._meta.verbose_name,
358
+ "form": form,
359
+ "return_url": self.get_return_url(request, obj),
360
+ "editing": obj.present_in_database,
361
+ "active_tab": "assign",
362
+ **self.get_extra_context(request, obj),
363
+ },
364
+ )
365
+
366
+ def post(self, request, *args, **kwargs):
367
+ obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
368
+ form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
369
+ restrict_form_fields(form, request.user)
370
+
371
+ associated_object_id = obj.pk
372
+ associated_object_content_type = ContentType.objects.get_for_model(Location)
373
+ action = request.POST.get("action")
374
+ try:
375
+ with transaction.atomic():
376
+ if not has_perms(request.user, ["extras.add_contactassociation"]):
377
+ raise PermissionDenied(
378
+ "ObjectPermission extras.add_contactassociation is needed to perform this action"
379
+ )
380
+ contact = None
381
+ team = None
382
+ if action == LocationDataToContactActionChoices.CREATE_AND_ASSIGN_NEW_CONTACT:
383
+ if not has_perms(request.user, ["extras.add_contact"]):
384
+ raise PermissionDenied("ObjectPermission extras.add_contact is needed to perform this action")
385
+ contact = Contact(
386
+ name=request.POST.get("name"),
387
+ phone=request.POST.get("phone"),
388
+ email=request.POST.get("email"),
389
+ )
390
+ contact.validated_save()
391
+ # Trigger permission check
392
+ Contact.objects.restrict(request.user, "view").get(pk=contact.pk)
393
+ elif action == LocationDataToContactActionChoices.CREATE_AND_ASSIGN_NEW_TEAM:
394
+ if not has_perms(request.user, ["extras.add_team"]):
395
+ raise PermissionDenied("ObjectPermission extras.add_team is needed to perform this action")
396
+ team = Team(
397
+ name=request.POST.get("name"),
398
+ phone=request.POST.get("phone"),
399
+ email=request.POST.get("email"),
400
+ )
401
+ team.validated_save()
402
+ # Trigger permission check
403
+ Team.objects.restrict(request.user, "view").get(pk=team.pk)
404
+ elif action == LocationDataToContactActionChoices.ASSOCIATE_EXISTING_CONTACT:
405
+ contact = Contact.objects.restrict(request.user, "view").get(pk=request.POST.get("contact"))
406
+ elif action == LocationDataToContactActionChoices.ASSOCIATE_EXISTING_TEAM:
407
+ team = Team.objects.restrict(request.user, "view").get(pk=request.POST.get("team"))
408
+ else:
409
+ raise ValueError(f"Invalid action {action} passed from the form")
410
+
411
+ association = ContactAssociation(
412
+ contact=contact,
413
+ team=team,
414
+ associated_object_type=associated_object_content_type,
415
+ associated_object_id=associated_object_id,
416
+ status=Status.objects.get(pk=request.POST.get("status")),
417
+ role=Role.objects.get(pk=request.POST.get("role")),
418
+ )
419
+ association.validated_save()
420
+ # Trigger permission check
421
+ ContactAssociation.objects.restrict(request.user, "view").get(pk=association.pk)
422
+
423
+ # Clear out contact fields from location
424
+ location = self.get_object(kwargs)
425
+ location.contact_name = ""
426
+ location.contact_phone = ""
427
+ location.contact_email = ""
428
+ location.validated_save()
429
+
430
+ object_created = not form.instance.present_in_database
431
+
432
+ self.successful_post(request, obj, object_created, logger)
433
+
434
+ return_url = request.POST.get("return_url")
435
+ if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
436
+ return redirect(iri_to_uri(return_url))
437
+ else:
438
+ return redirect(self.get_return_url(request, obj))
439
+
440
+ except ObjectDoesNotExist:
441
+ msg = "Object save failed due to object-level permissions violation"
442
+ logger.debug(msg)
443
+ form.add_error(None, msg)
444
+ except PermissionDenied as e:
445
+ msg = e
446
+ logger.debug(msg)
447
+ form.add_error(None, msg)
448
+ except ValueError:
449
+ msg = f"Invalid action {action} passed from the form"
450
+ logger.debug(msg)
451
+ form.add_error(None, msg)
452
+
453
+ return render(
454
+ request,
455
+ self.template_name,
456
+ {
457
+ "obj": obj,
458
+ "obj_type": self.queryset.model._meta.verbose_name,
459
+ "form": form,
460
+ "return_url": self.get_return_url(request, obj),
461
+ "editing": obj.present_in_database,
462
+ **self.get_extra_context(request, obj),
463
+ },
464
+ )
465
+
466
+
317
467
  #
318
468
  # Rack groups
319
469
  #
@@ -346,9 +496,7 @@ class RackGroupView(generic.ObjectView):
346
496
  }
347
497
  RequestConfig(request, paginate).configure(rack_table)
348
498
 
349
- return {
350
- "rack_table": rack_table,
351
- }
499
+ return {"rack_table": rack_table, **super().get_extra_context(request, instance)}
352
500
 
353
501
 
354
502
  class RackGroupEditView(generic.ObjectEditView):
@@ -467,6 +615,7 @@ class RackView(generic.ObjectView):
467
615
  "nonracked_devices": nonracked_devices,
468
616
  "next_rack": next_rack,
469
617
  "prev_rack": prev_rack,
618
+ **super().get_extra_context(request, instance),
470
619
  }
471
620
 
472
621
 
@@ -583,9 +732,7 @@ class ManufacturerView(generic.ObjectView):
583
732
  }
584
733
  RequestConfig(request, paginate).configure(device_table)
585
734
 
586
- return {
587
- "device_table": device_table,
588
- }
735
+ return {"device_table": device_table, **super().get_extra_context(request, instance)}
589
736
 
590
737
 
591
738
  class ManufacturerEditView(generic.ObjectEditView):
@@ -691,6 +838,7 @@ class DeviceTypeView(generic.ObjectView):
691
838
  "rear_port_table": rear_port_table,
692
839
  "devicebay_table": devicebay_table,
693
840
  "software_image_files_table": software_image_files_table,
841
+ **super().get_extra_context(request, instance),
694
842
  }
695
843
 
696
844
 
@@ -1089,6 +1237,7 @@ class PlatformView(generic.ObjectView):
1089
1237
  return {
1090
1238
  "device_table": device_table,
1091
1239
  "network_driver_tool_names": get_network_driver_mapping_tool_names(),
1240
+ **super().get_extra_context(request, instance),
1092
1241
  }
1093
1242
 
1094
1243
 
@@ -1098,7 +1247,10 @@ class PlatformEditView(generic.ObjectEditView):
1098
1247
  template_name = "dcim/platform_edit.html"
1099
1248
 
1100
1249
  def get_extra_context(self, request, instance):
1101
- return {"network_driver_names": sorted(get_all_network_driver_mappings().keys())}
1250
+ return {
1251
+ "network_driver_names": sorted(get_all_network_driver_mappings().keys()),
1252
+ **super().get_extra_context(request, instance),
1253
+ }
1102
1254
 
1103
1255
 
1104
1256
  class PlatformDeleteView(generic.ObjectDeleteView):
@@ -1489,7 +1641,7 @@ class ConsolePortView(generic.ObjectView):
1489
1641
  queryset = ConsolePort.objects.all()
1490
1642
 
1491
1643
  def get_extra_context(self, request, instance):
1492
- return {"breadcrumb_url": "dcim:device_consoleports"}
1644
+ return {"breadcrumb_url": "dcim:device_consoleports", **super().get_extra_context(request, instance)}
1493
1645
 
1494
1646
 
1495
1647
  class ConsolePortCreateView(generic.ComponentCreateView):
@@ -1551,7 +1703,7 @@ class ConsoleServerPortView(generic.ObjectView):
1551
1703
  queryset = ConsoleServerPort.objects.all()
1552
1704
 
1553
1705
  def get_extra_context(self, request, instance):
1554
- return {"breadcrumb_url": "dcim:device_consoleserverports"}
1706
+ return {"breadcrumb_url": "dcim:device_consoleserverports", **super().get_extra_context(request, instance)}
1555
1707
 
1556
1708
 
1557
1709
  class ConsoleServerPortCreateView(generic.ComponentCreateView):
@@ -1613,7 +1765,7 @@ class PowerPortView(generic.ObjectView):
1613
1765
  queryset = PowerPort.objects.all()
1614
1766
 
1615
1767
  def get_extra_context(self, request, instance):
1616
- return {"breadcrumb_url": "dcim:device_powerports"}
1768
+ return {"breadcrumb_url": "dcim:device_powerports", **super().get_extra_context(request, instance)}
1617
1769
 
1618
1770
 
1619
1771
  class PowerPortCreateView(generic.ComponentCreateView):
@@ -1675,7 +1827,7 @@ class PowerOutletView(generic.ObjectView):
1675
1827
  queryset = PowerOutlet.objects.all()
1676
1828
 
1677
1829
  def get_extra_context(self, request, instance):
1678
- return {"breadcrumb_url": "dcim:device_poweroutlets"}
1830
+ return {"breadcrumb_url": "dcim:device_poweroutlets", **super().get_extra_context(request, instance)}
1679
1831
 
1680
1832
 
1681
1833
  class PowerOutletCreateView(generic.ComponentCreateView):
@@ -1771,6 +1923,7 @@ class InterfaceView(generic.ObjectView):
1771
1923
  "breadcrumb_url": "dcim:device_interfaces",
1772
1924
  "child_interfaces_table": child_interfaces_tables,
1773
1925
  "redundancy_table": redundancy_table,
1926
+ **super().get_extra_context(request, instance),
1774
1927
  }
1775
1928
 
1776
1929
  def _get_interface_redundancy_groups_table(self, request, instance):
@@ -1857,7 +2010,7 @@ class FrontPortView(generic.ObjectView):
1857
2010
  queryset = FrontPort.objects.all()
1858
2011
 
1859
2012
  def get_extra_context(self, request, instance):
1860
- return {"breadcrumb_url": "dcim:device_frontports"}
2013
+ return {"breadcrumb_url": "dcim:device_frontports", **super().get_extra_context(request, instance)}
1861
2014
 
1862
2015
 
1863
2016
  class FrontPortCreateView(generic.ComponentCreateView):
@@ -1919,7 +2072,7 @@ class RearPortView(generic.ObjectView):
1919
2072
  queryset = RearPort.objects.all()
1920
2073
 
1921
2074
  def get_extra_context(self, request, instance):
1922
- return {"breadcrumb_url": "dcim:device_rearports"}
2075
+ return {"breadcrumb_url": "dcim:device_rearports", **super().get_extra_context(request, instance)}
1923
2076
 
1924
2077
 
1925
2078
  class RearPortCreateView(generic.ComponentCreateView):
@@ -1981,7 +2134,7 @@ class DeviceBayView(generic.ObjectView):
1981
2134
  queryset = DeviceBay.objects.all()
1982
2135
 
1983
2136
  def get_extra_context(self, request, instance):
1984
- return {"breadcrumb_url": "dcim:device_devicebays"}
2137
+ return {"breadcrumb_url": "dcim:device_devicebays", **super().get_extra_context(request, instance)}
1985
2138
 
1986
2139
 
1987
2140
  class DeviceBayCreateView(generic.ComponentCreateView):
@@ -2133,6 +2286,7 @@ class InventoryItemView(generic.ObjectView):
2133
2286
  return {
2134
2287
  "breadcrumb_url": "dcim:device_inventory",
2135
2288
  "software_version_images": software_version_images,
2289
+ **super().get_extra_context(request, instance),
2136
2290
  }
2137
2291
 
2138
2292
 
@@ -2340,6 +2494,7 @@ class PathTraceView(generic.ObjectView):
2340
2494
  "path": path,
2341
2495
  "related_paths": related_paths,
2342
2496
  "total_length": path.get_total_length() if path else None,
2497
+ **super().get_extra_context(request, instance),
2343
2498
  }
2344
2499
 
2345
2500
 
@@ -2550,9 +2705,7 @@ class VirtualChassisView(generic.ObjectView):
2550
2705
  def get_extra_context(self, request, instance):
2551
2706
  members = Device.objects.restrict(request.user).filter(virtual_chassis=instance)
2552
2707
 
2553
- return {
2554
- "members": members,
2555
- }
2708
+ return {"members": members, **super().get_extra_context(request, instance)}
2556
2709
 
2557
2710
 
2558
2711
  class VirtualChassisCreateView(generic.ObjectEditView):
@@ -2790,9 +2943,7 @@ class PowerPanelView(generic.ObjectView):
2790
2943
  powerfeed_table = tables.PowerFeedTable(data=power_feeds, orderable=False)
2791
2944
  powerfeed_table.exclude = ["power_panel"]
2792
2945
 
2793
- return {
2794
- "powerfeed_table": powerfeed_table,
2795
- }
2946
+ return {"powerfeed_table": powerfeed_table, **super().get_extra_context(request, instance)}
2796
2947
 
2797
2948
 
2798
2949
  class PowerPanelEditView(generic.ObjectEditView):
@@ -223,17 +223,28 @@ class ContactAssociationSerializer(NautobotModelSerializer):
223
223
  }
224
224
 
225
225
  def validate(self, data):
226
- # Validate uniqueness of (contact/team, role)
226
+ # Validate uniqueness of (associated object, associated object type, contact/team, role)
227
+ unique_together_fields = None
228
+
227
229
  if data.get("contact") and data.get("role"):
228
- validator = UniqueTogetherValidator(
229
- queryset=ContactAssociation.objects.all(),
230
- fields=("contact", "role"),
230
+ unique_together_fields = (
231
+ "associated_object_type",
232
+ "associated_object_id",
233
+ "contact",
234
+ "role",
231
235
  )
232
- validator(data, self)
233
236
  elif data.get("team") and data.get("role"):
237
+ unique_together_fields = (
238
+ "associated_object_type",
239
+ "associated_object_id",
240
+ "team",
241
+ "role",
242
+ )
243
+
244
+ if unique_together_fields is not None:
234
245
  validator = UniqueTogetherValidator(
235
246
  queryset=ContactAssociation.objects.all(),
236
- fields=("team", "role"),
247
+ fields=unique_together_fields,
237
248
  )
238
249
  validator(data, self)
239
250
 
@@ -33,6 +33,7 @@ from nautobot.core.models.querysets import count_related
33
33
  from nautobot.extras import filters
34
34
  from nautobot.extras.choices import JobExecutionType
35
35
  from nautobot.extras.filters import RoleFilterSet
36
+ from nautobot.extras.jobs import get_job
36
37
  from nautobot.extras.models import (
37
38
  ComputedField,
38
39
  ConfigContext,
@@ -484,7 +485,7 @@ def _create_schedule(serializer, data, job_model, user, approval_required, task_
484
485
  # scheduled for.
485
486
  scheduled_job = ScheduledJob(
486
487
  name=name,
487
- task=job_model.job_class.registered_name,
488
+ task=job_model.class_path,
488
489
  job_model=job_model,
489
490
  start_time=time,
490
491
  description=f"Nautobot job {name} scheduled by {user} for {time}",
@@ -520,7 +521,7 @@ class JobViewSetBase(
520
521
  def variables(self, request, *args, **kwargs):
521
522
  """Get details of the input variables that may/must be specified to run a particular Job."""
522
523
  job_model = self.get_object()
523
- job_class = job_model.job_class
524
+ job_class = get_job(job_model.class_path, reload=True)
524
525
  if job_class is None:
525
526
  raise Http404
526
527
  variables_dict = job_class._get_vars()
@@ -602,14 +603,15 @@ class JobViewSetBase(
602
603
  "One of these two flags must be removed before this job can be scheduled or run."
603
604
  )
604
605
 
605
- job_class = job_model.job_class
606
+ job_class = get_job(job_model.class_path, reload=True)
606
607
  if job_class is None:
607
608
  raise MethodNotAllowed(
608
609
  request.method, detail="This job's source code could not be located and cannot be run"
609
610
  )
610
611
 
611
612
  valid_queues = job_model.task_queues if job_model.task_queues else [settings.CELERY_TASK_DEFAULT_QUEUE]
612
- # Get a default queue from either the job model's specified task queue or system default to fall back on if request doesn't provide one
613
+ # Get a default queue from either the job model's specified task queue or
614
+ # the system default to fall back on if request doesn't provide one
613
615
  default_valid_queue = valid_queues[0]
614
616
 
615
617
  # We need to call request.data for both cases as this is what pulls and caches the request data
@@ -621,7 +623,8 @@ class JobViewSetBase(
621
623
  # - Job Form data (for submission to the job itself)
622
624
  # - Schedule data
623
625
  # - Desired task queue
624
- # Depending on request content type (largely for backwards compatibility) the keys at which these are found are different
626
+ # Depending on request content type (largely for backwards compatibility) the keys at which these are found
627
+ # are different
625
628
  if "multipart/form-data" in request.content_type:
626
629
  data = request._data.dict() # .data will return data and files, we just want the data
627
630
  files = request.FILES
@@ -639,7 +642,8 @@ class JobViewSetBase(
639
642
  for non_job_key in non_job_keys:
640
643
  data.pop(non_job_key, None)
641
644
 
642
- # List of keys in serializer that are effectively exploded versions of the schedule dictionary from JobInputSerializer
645
+ # List of keys in serializer that are effectively exploded versions of the schedule dictionary
646
+ # from JobInputSerializer
643
647
  schedule_keys = ("_schedule_name", "_schedule_start_time", "_schedule_interval", "_schedule_crontab")
644
648
 
645
649
  # Assign the key from the validated_data output to dictionary without prefixed "_schedule_"
@@ -666,7 +670,7 @@ class JobViewSetBase(
666
670
  cleaned_data = None
667
671
  try:
668
672
  cleaned_data = job_class.validate_data(data, files=files)
669
- cleaned_data = job_model.job_class.prepare_job_kwargs(cleaned_data)
673
+ cleaned_data = job_class.prepare_job_kwargs(cleaned_data)
670
674
 
671
675
  except FormsValidationError as e:
672
676
  # message_dict can only be accessed if ValidationError got a dict
@@ -948,7 +952,13 @@ class ScheduledJobViewSet(ReadOnlyModelViewSet):
948
952
  responses={"200": serializers.JobResultSerializer},
949
953
  request=None,
950
954
  )
951
- @action(detail=True, url_path="dry-run", methods=["post"], permission_classes=[ScheduledJobViewPermissions])
955
+ @action(
956
+ detail=True,
957
+ name="Dry Run",
958
+ url_path="dry-run",
959
+ methods=["post"],
960
+ permission_classes=[ScheduledJobViewPermissions],
961
+ )
952
962
  def dry_run(self, request, pk):
953
963
  scheduled_job = get_object_or_404(ScheduledJob, pk=pk)
954
964
  job_model = scheduled_job.job_model
@@ -960,13 +970,14 @@ class ScheduledJobViewSet(ReadOnlyModelViewSet):
960
970
  raise PermissionDenied("You do not have permission to run this job.")
961
971
 
962
972
  # Immediately enqueue the job
963
- job_kwargs = job_model.job_class.prepare_job_kwargs(scheduled_job.kwargs.get("data", {}))
973
+ job_class = get_job(job_model.class_path, reload=True)
974
+ job_kwargs = job_class.prepare_job_kwargs(scheduled_job.kwargs or {})
964
975
  job_kwargs["dryrun"] = True
965
976
  job_result = JobResult.enqueue_job(
966
977
  job_model,
967
978
  request.user,
968
979
  celery_kwargs=scheduled_job.celery_kwargs or {},
969
- **job_model.job_class.serialize_data(job_kwargs),
980
+ **job_class.serialize_data(job_kwargs),
970
981
  )
971
982
  serializer = serializers.JobResultSerializer(job_result, context={"request": request})
972
983
 
@@ -5,16 +5,16 @@ HTTP_CONTENT_TYPE_JSON = "application/json"
5
5
  EXTRAS_FEATURES = [
6
6
  "cable_terminations",
7
7
  "config_context_owners",
8
- "custom_fields",
8
+ "custom_fields", # Deprecated - see nautobot.extras.utils.populate_model_features_registry
9
9
  "custom_links",
10
10
  "custom_validators",
11
11
  "dynamic_groups",
12
12
  "export_template_owners",
13
13
  "export_templates",
14
14
  "graphql",
15
- "job_results",
15
+ "job_results", # No longer used
16
16
  "locations",
17
- "relationships",
17
+ "relationships", # Deprecated - see nautobot.extras.utils.populate_model_features_registry
18
18
  "statuses",
19
19
  "webhooks",
20
20
  ]