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/extras/views.py CHANGED
@@ -152,19 +152,18 @@ class ConfigContextView(generic.ObjectView):
152
152
  queryset = ConfigContext.objects.all()
153
153
 
154
154
  def get_extra_context(self, request, instance):
155
+ context = super().get_extra_context(request, instance)
155
156
  # Determine user's preferred output format
156
157
  if request.GET.get("format") in ["json", "yaml"]:
157
- format_ = request.GET.get("format")
158
+ context["format"] = request.GET.get("format")
158
159
  if request.user.is_authenticated:
159
- request.user.set_config("extras.configcontext.format", format_, commit=True)
160
+ request.user.set_config("extras.configcontext.format", context["format"], commit=True)
160
161
  elif request.user.is_authenticated:
161
- format_ = request.user.get_config("extras.configcontext.format", "json")
162
+ context["format"] = request.user.get_config("extras.configcontext.format", "json")
162
163
  else:
163
- format_ = "json"
164
+ context["format"] = "json"
164
165
 
165
- return {
166
- "format": format_,
167
- }
166
+ return context
168
167
 
169
168
 
170
169
  class ConfigContextEditView(generic.ObjectEditView):
@@ -236,19 +235,18 @@ class ConfigContextSchemaView(generic.ObjectView):
236
235
  queryset = ConfigContextSchema.objects.all()
237
236
 
238
237
  def get_extra_context(self, request, instance):
238
+ context = super().get_extra_context(request, instance)
239
239
  # Determine user's preferred output format
240
240
  if request.GET.get("format") in ["json", "yaml"]:
241
- format_ = request.GET.get("format")
241
+ context["format"] = request.GET.get("format")
242
242
  if request.user.is_authenticated:
243
- request.user.set_config("extras.configcontextschema.format", format_, commit=True)
243
+ request.user.set_config("extras.configcontextschema.format", context["format"], commit=True)
244
244
  elif request.user.is_authenticated:
245
- format_ = request.user.get_config("extras.configcontextschema.format", "json")
245
+ context["format"] = request.user.get_config("extras.configcontextschema.format", "json")
246
246
  else:
247
- format_ = "json"
247
+ context["format"] = "json"
248
248
 
249
- return {
250
- "format": format_,
251
- }
249
+ return context
252
250
 
253
251
 
254
252
  class ConfigContextSchemaObjectValidationView(generic.ObjectView):
@@ -404,10 +402,8 @@ class ContactAssociationUIViewSet(
404
402
  non_filter_params = ("export", "page", "per_page", "sort")
405
403
 
406
404
 
407
- class ObjectNewContactView(generic.ObjectEditView):
408
- queryset = Contact.objects.all()
409
- model_form = forms.ObjectNewContactForm
410
- template_name = "extras/object_new_contact.html"
405
+ class ObjectContactTeamMixin:
406
+ """Mixin that contains a custom post() method to create a new contact/team and assign it to an existing object"""
411
407
 
412
408
  def post(self, request, *args, **kwargs):
413
409
  obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
@@ -428,13 +424,22 @@ class ObjectNewContactView(generic.ObjectEditView):
428
424
  if hasattr(form, "save_note") and callable(form.save_note):
429
425
  form.save_note(instance=obj, user=request.user)
430
426
 
431
- association = ContactAssociation(
432
- contact=obj,
433
- associated_object_type=ContentType.objects.get(id=request.POST.get("associated_object_type")),
434
- associated_object_id=request.POST.get("associated_object_id"),
435
- status=Status.objects.get(id=request.POST.get("status")),
436
- role=Role.objects.get(id=request.POST.get("role")) if request.POST.get("role") else None,
437
- )
427
+ if isinstance(obj, Contact):
428
+ association = ContactAssociation(
429
+ contact=obj,
430
+ associated_object_type=ContentType.objects.get(id=request.POST.get("associated_object_type")),
431
+ associated_object_id=request.POST.get("associated_object_id"),
432
+ status=Status.objects.get(id=request.POST.get("status")),
433
+ role=Role.objects.get(id=request.POST.get("role")) if request.POST.get("role") else None,
434
+ )
435
+ else:
436
+ association = ContactAssociation(
437
+ team=obj,
438
+ associated_object_type=ContentType.objects.get(id=request.POST.get("associated_object_type")),
439
+ associated_object_id=request.POST.get("associated_object_id"),
440
+ status=Status.objects.get(id=request.POST.get("status")),
441
+ role=Role.objects.get(id=request.POST.get("role")) if request.POST.get("role") else None,
442
+ )
438
443
  association.validated_save()
439
444
  self.successful_post(request, obj, object_created, logger)
440
445
 
@@ -474,75 +479,17 @@ class ObjectNewContactView(generic.ObjectEditView):
474
479
  )
475
480
 
476
481
 
477
- class ObjectNewTeamView(generic.ObjectEditView):
482
+ class ObjectNewContactView(ObjectContactTeamMixin, generic.ObjectEditView):
483
+ queryset = Contact.objects.all()
484
+ model_form = forms.ObjectNewContactForm
485
+ template_name = "extras/object_new_contact.html"
486
+
487
+
488
+ class ObjectNewTeamView(ObjectContactTeamMixin, generic.ObjectEditView):
478
489
  queryset = Team.objects.all()
479
490
  model_form = forms.ObjectNewTeamForm
480
491
  template_name = "extras/object_new_team.html"
481
492
 
482
- def post(self, request, *args, **kwargs):
483
- obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
484
- form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
485
- restrict_form_fields(form, request.user)
486
-
487
- if form.is_valid():
488
- logger.debug("Form validation was successful")
489
-
490
- try:
491
- with transaction.atomic():
492
- object_created = not form.instance.present_in_database
493
- obj = form.save()
494
-
495
- # Check that the new object conforms with any assigned object-level permissions
496
- self.queryset.get(pk=obj.pk)
497
-
498
- if hasattr(form, "save_note") and callable(form.save_note):
499
- form.save_note(instance=obj, user=request.user)
500
-
501
- association = ContactAssociation(
502
- team=obj,
503
- associated_object_type=ContentType.objects.get(id=request.POST.get("associated_object_type")),
504
- associated_object_id=request.POST.get("associated_object_id"),
505
- status=Status.objects.get(id=request.POST.get("status")),
506
- role=Role.objects.get(id=request.POST.get("role")) if request.POST.get("role") else None,
507
- )
508
- association.validated_save()
509
- self.successful_post(request, obj, object_created, logger)
510
-
511
- if "_addanother" in request.POST:
512
- # If the object has clone_fields, pre-populate a new instance of the form
513
- if hasattr(obj, "clone_fields"):
514
- url = f"{request.path}?{prepare_cloned_fields(obj)}"
515
- return redirect(url)
516
-
517
- return redirect(request.get_full_path())
518
-
519
- return_url = form.cleaned_data.get("return_url")
520
- if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
521
- return redirect(iri_to_uri(return_url))
522
- else:
523
- return redirect(self.get_return_url(request, obj))
524
-
525
- except ObjectDoesNotExist:
526
- msg = "Object save failed due to object-level permissions violation"
527
- logger.debug(msg)
528
- form.add_error(None, msg)
529
-
530
- else:
531
- logger.debug("Form validation failed")
532
-
533
- return render(
534
- request,
535
- self.template_name,
536
- {
537
- "obj": obj,
538
- "obj_type": self.queryset.model._meta.verbose_name,
539
- "form": form,
540
- "return_url": self.get_return_url(request, obj),
541
- "editing": obj.present_in_database,
542
- **self.get_extra_context(request, obj),
543
- },
544
- )
545
-
546
493
 
547
494
  class ObjectAssignContactOrTeamView(generic.ObjectEditView):
548
495
  queryset = ContactAssociation.objects.all()
@@ -1045,6 +992,7 @@ class GitRepositoryView(generic.ObjectView):
1045
992
  def get_extra_context(self, request, instance):
1046
993
  return {
1047
994
  "datasource_contents": get_datasource_contents("extras.gitrepository"),
995
+ **super().get_extra_context(request, instance),
1048
996
  }
1049
997
 
1050
998
 
@@ -1250,7 +1198,10 @@ class JobListView(generic.ObjectListView):
1250
1198
  filterset = filters.JobFilterSet
1251
1199
  filterset_form = forms.JobFilterForm
1252
1200
  action_buttons = ()
1253
- non_filter_params = ("display",)
1201
+ non_filter_params = (
1202
+ *generic.ObjectListView.non_filter_params,
1203
+ "display",
1204
+ )
1254
1205
  template_name = "extras/job_list.html"
1255
1206
 
1256
1207
  def alter_queryset(self, request):
@@ -1305,11 +1256,9 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1305
1256
  job_model = self._get_job_model_or_404(class_path, pk)
1306
1257
 
1307
1258
  try:
1308
- try:
1309
- job_class = job_model.job_class
1310
- except TypeError as exc:
1311
- # job_class may be None
1312
- raise RuntimeError("Job code for this job is not currently installed or loadable") from exc
1259
+ job_class = get_job(job_model.class_path, reload=True)
1260
+ if job_class is None:
1261
+ raise RuntimeError("Job code for this job is not currently installed or loadable")
1313
1262
  initial = normalize_querydict(request.GET, form_class=job_class.as_form_class())
1314
1263
  if "kwargs_from_job_result" in initial:
1315
1264
  job_result_pk = initial.pop("kwargs_from_job_result")
@@ -1357,7 +1306,8 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1357
1306
  def post(self, request, class_path=None, pk=None):
1358
1307
  job_model = self._get_job_model_or_404(class_path, pk)
1359
1308
 
1360
- job_form = job_model.job_class.as_form(request.POST, request.FILES) if job_model.job_class is not None else None
1309
+ job_class = get_job(job_model.class_path, reload=True)
1310
+ job_form = job_class.as_form(request.POST, request.FILES) if job_class is not None else None
1361
1311
  schedule_form = forms.JobScheduleForm(request.POST)
1362
1312
  task_queue = request.POST.get("_task_queue")
1363
1313
 
@@ -1370,7 +1320,7 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1370
1320
  # Allow execution only if a worker process is running and the job is runnable.
1371
1321
  if not get_worker_count(queue=task_queue):
1372
1322
  messages.error(request, "Unable to run or schedule job: Celery worker process not running.")
1373
- elif not job_model.installed or job_model.job_class is None:
1323
+ elif not job_model.installed or job_class is None:
1374
1324
  messages.error(request, "Unable to run or schedule job: Job is not presently installed.")
1375
1325
  elif not job_model.enabled:
1376
1326
  messages.error(request, "Unable to run or schedule job: Job is not enabled to be run.")
@@ -1422,11 +1372,11 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1422
1372
  celery_kwargs = {"nautobot_job_profile": profile, "queue": task_queue}
1423
1373
  scheduled_job = ScheduledJob(
1424
1374
  name=schedule_name,
1425
- task=job_model.job_class.registered_name,
1375
+ task=job_model.class_path,
1426
1376
  job_model=job_model,
1427
1377
  start_time=schedule_datetime,
1428
1378
  description=f"Nautobot job {schedule_name} scheduled by {request.user} for {schedule_datetime}",
1429
- kwargs=job_model.job_class.serialize_data(job_form.cleaned_data),
1379
+ kwargs=job_class.serialize_data(job_form.cleaned_data),
1430
1380
  celery_kwargs=celery_kwargs,
1431
1381
  interval=schedule_type,
1432
1382
  one_off=schedule_type == JobExecutionType.TYPE_FUTURE,
@@ -1446,13 +1396,13 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1446
1396
 
1447
1397
  else:
1448
1398
  # Enqueue job for immediate execution
1449
- job_kwargs = job_model.job_class.prepare_job_kwargs(job_form.cleaned_data)
1399
+ job_kwargs = job_class.prepare_job_kwargs(job_form.cleaned_data)
1450
1400
  job_result = JobResult.enqueue_job(
1451
1401
  job_model,
1452
1402
  request.user,
1453
1403
  profile=profile,
1454
1404
  task_queue=task_queue,
1455
- **job_model.job_class.serialize_data(job_kwargs),
1405
+ **job_class.serialize_data(job_kwargs),
1456
1406
  )
1457
1407
 
1458
1408
  if return_url:
@@ -1471,10 +1421,10 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
1471
1421
  return redirect(return_url)
1472
1422
 
1473
1423
  template_name = "extras/job.html"
1474
- if job_model.job_class is not None and hasattr(job_model.job_class, "template_name"):
1424
+ if job_class is not None and hasattr(job_class, "template_name"):
1475
1425
  try:
1476
- get_template(job_model.job_class.template_name)
1477
- template_name = job_model.job_class.template_name
1426
+ get_template(job_class.template_name)
1427
+ template_name = job_class.template_name
1478
1428
  except TemplateDoesNotExist as err:
1479
1429
  messages.error(request, f'Unable to render requested custom job template "{template_name}": {err}')
1480
1430
 
@@ -1555,7 +1505,7 @@ class JobApprovalRequestView(generic.ObjectView):
1555
1505
  """
1556
1506
  job_model = instance.job_model
1557
1507
  if job_model is not None:
1558
- job_class = job_model.job_class
1508
+ job_class = get_job(job_model.class_path, reload=True)
1559
1509
  else:
1560
1510
  # 2.0 TODO: remove this fallback?
1561
1511
  job_class = get_job(instance.job_class)
@@ -1569,9 +1519,7 @@ class JobApprovalRequestView(generic.ObjectView):
1569
1519
  else:
1570
1520
  job_form = None
1571
1521
 
1572
- return {
1573
- "job_form": job_form,
1574
- }
1522
+ return {"job_form": job_form, **super().get_extra_context(request, instance)}
1575
1523
 
1576
1524
  def post(self, request, pk):
1577
1525
  """
@@ -1591,6 +1539,7 @@ class JobApprovalRequestView(generic.ObjectView):
1591
1539
  dry_run = "_dry_run" in post_data
1592
1540
 
1593
1541
  job_model = scheduled_job.job_model
1542
+ job_class = get_job(job_model.class_path, reload=True)
1594
1543
 
1595
1544
  if dry_run:
1596
1545
  # To dry-run a job, a user needs the same permissions that would be needed to run the job directly
@@ -1604,13 +1553,13 @@ class JobApprovalRequestView(generic.ObjectView):
1604
1553
  messages.error(request, "This job does not support dryrun")
1605
1554
  else:
1606
1555
  # Immediately enqueue the job and send the user to the normal JobResult view
1607
- job_kwargs = job_model.job_class.prepare_job_kwargs(scheduled_job.kwargs or {})
1556
+ job_kwargs = job_class.prepare_job_kwargs(scheduled_job.kwargs or {})
1608
1557
  job_kwargs["dryrun"] = True
1609
1558
  job_result = JobResult.enqueue_job(
1610
1559
  job_model,
1611
1560
  request.user,
1612
1561
  celery_kwargs=scheduled_job.celery_kwargs,
1613
- **job_model.job_class.serialize_data(job_kwargs),
1562
+ **job_class.serialize_data(job_kwargs),
1614
1563
  )
1615
1564
 
1616
1565
  return redirect("extras:jobresult", pk=job_result.pk)
@@ -1697,10 +1646,14 @@ class ScheduledJobView(generic.ObjectView):
1697
1646
  for name, var in job_class._get_vars().items():
1698
1647
  field = var.as_field()
1699
1648
  if field.label:
1700
- labels[name] = var
1649
+ labels[name] = field.label
1701
1650
  else:
1702
1651
  labels[name] = pretty_name(name)
1703
- return {"labels": labels, "job_class_found": (job_class is not None)}
1652
+ return {
1653
+ "labels": labels,
1654
+ "job_class_found": (job_class is not None),
1655
+ **super().get_extra_context(request, instance),
1656
+ }
1704
1657
 
1705
1658
 
1706
1659
  class ScheduledJobDeleteView(generic.ObjectDeleteView):
@@ -1724,7 +1677,10 @@ class JobHookView(generic.ObjectView):
1724
1677
  queryset = JobHook.objects.all()
1725
1678
 
1726
1679
  def get_extra_context(self, request, instance):
1727
- return {"content_types": instance.content_types.order_by("app_label", "model")}
1680
+ return {
1681
+ "content_types": instance.content_types.order_by("app_label", "model"),
1682
+ **super().get_extra_context(request, instance),
1683
+ }
1728
1684
 
1729
1685
 
1730
1686
  class JobHookEditView(generic.ObjectEditView):
@@ -1809,6 +1765,7 @@ class JobResultView(generic.ObjectView):
1809
1765
  "job": job_class,
1810
1766
  "associated_record": associated_record,
1811
1767
  "result": instance,
1768
+ **super().get_extra_context(request, instance),
1812
1769
  }
1813
1770
 
1814
1771
 
@@ -1896,6 +1853,7 @@ class ObjectChangeView(generic.ObjectView):
1896
1853
  "prev_change": instance.get_prev_change(request.user),
1897
1854
  "related_changes_table": related_changes_table,
1898
1855
  "related_changes_count": related_changes.count(),
1856
+ **super().get_extra_context(request, instance),
1899
1857
  }
1900
1858
 
1901
1859
 
@@ -2240,6 +2198,7 @@ class SecretView(generic.ObjectView):
2240
2198
  "format": format_,
2241
2199
  "provider_name": provider.name if provider else instance.provider,
2242
2200
  "groups_table": groups_table,
2201
+ **super().get_extra_context(request, instance),
2243
2202
  }
2244
2203
 
2245
2204
 
@@ -2455,7 +2414,10 @@ class StatusView(generic.ObjectView):
2455
2414
 
2456
2415
  def get_extra_context(self, request, instance):
2457
2416
  """Return ordered content types."""
2458
- return {"content_types": instance.content_types.order_by("app_label", "model")}
2417
+ return {
2418
+ "content_types": instance.content_types.order_by("app_label", "model"),
2419
+ **super().get_extra_context(request, instance),
2420
+ }
2459
2421
 
2460
2422
 
2461
2423
  #
@@ -2490,6 +2452,7 @@ class TagView(generic.ObjectView):
2490
2452
  "items_count": tagged_items.count(),
2491
2453
  "items_table": items_table,
2492
2454
  "content_types": instance.content_types.order_by("app_label", "model"),
2455
+ **super().get_extra_context(request, instance),
2493
2456
  }
2494
2457
 
2495
2458
 
@@ -2570,7 +2533,10 @@ class WebhookView(generic.ObjectView):
2570
2533
  queryset = Webhook.objects.all()
2571
2534
 
2572
2535
  def get_extra_context(self, request, instance):
2573
- return {"content_types": instance.content_types.order_by("app_label", "model")}
2536
+ return {
2537
+ "content_types": instance.content_types.order_by("app_label", "model"),
2538
+ **super().get_extra_context(request, instance),
2539
+ }
2574
2540
 
2575
2541
 
2576
2542
  class WebhookEditView(generic.ObjectEditView):
@@ -160,7 +160,13 @@ class PrefixViewSet(NautobotModelViewSet):
160
160
 
161
161
  @extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
162
162
  @extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=False)})
163
- @action(detail=True, url_path="available-prefixes", methods=["get", "post"], filterset_class=None)
163
+ @action(
164
+ detail=True,
165
+ name="Available Prefixes",
166
+ url_path="available-prefixes",
167
+ methods=["get", "post"],
168
+ filterset_class=None,
169
+ )
164
170
  def available_prefixes(self, request, pk=None):
165
171
  """
166
172
  A convenience method for returning available child prefixes within a parent.
@@ -237,6 +243,7 @@ class PrefixViewSet(NautobotModelViewSet):
237
243
  )
238
244
  @action(
239
245
  detail=True,
246
+ name="Available IPs",
240
247
  url_path="available-ips",
241
248
  methods=["get", "post"],
242
249
  queryset=IPAddress.objects.all(),
@@ -26,6 +26,7 @@ class PrefixType(OptimizedNautobotObjectType):
26
26
  prefix = graphene.String()
27
27
  ip_version = graphene.Int()
28
28
  dynamic_groups = graphene.List("nautobot.extras.graphql.types.DynamicGroupType")
29
+ location = graphene.Field("nautobot.dcim.graphql.types.LocationType")
29
30
 
30
31
  class Meta:
31
32
  model = models.Prefix
@@ -33,3 +34,13 @@ class PrefixType(OptimizedNautobotObjectType):
33
34
 
34
35
  def resolve_dynamic_groups(self, args):
35
36
  return DynamicGroup.objects.get_for_object(self, use_cache=True)
37
+
38
+
39
+ class VLANType(OptimizedNautobotObjectType):
40
+ """Graphql Type Object for VLAN model."""
41
+
42
+ location = graphene.Field("nautobot.dcim.graphql.types.LocationType")
43
+
44
+ class Meta:
45
+ model = models.VLAN
46
+ filterset_class = filters.VLANFilterSet
@@ -0,0 +1,32 @@
1
+ class LocationToLocationsQuerySetMixin:
2
+ """
3
+ A mixin for Django QuerySets to support backward compatibility by converting
4
+ queries from a previously used 'location' field to the new
5
+ 'locations'. This mixin intercepts `filter` and `exclude` calls
6
+ to transform references from 'location' to 'locations'.
7
+ """
8
+
9
+ def _convert_location_to_locations(self, kwargs):
10
+ """Transforms query parameters that reference 'location' field into the corresponding 'locations' field."""
11
+ updated_kwargs = {}
12
+ for field, value in kwargs.items():
13
+ if field == "location":
14
+ # If there is no lookup expression, it means 'location' is queried directly,
15
+ # thus use 'locations__in' to accommodate the ManyToMany relationship
16
+ updated_kwargs["locations__in"] = [value]
17
+ elif field.startswith("location__"):
18
+ # If there is a lookup expression following 'location', prepend it with 'locations'
19
+ _, lookup_expr = field.split("location", maxsplit=1)
20
+ locations_field = f"locations{lookup_expr}".strip()
21
+ updated_kwargs[locations_field] = value
22
+ else:
23
+ updated_kwargs[field] = value
24
+ return updated_kwargs
25
+
26
+ def filter(self, *args, **kwargs):
27
+ kwargs = self._convert_location_to_locations(kwargs)
28
+ return super().filter(*args, **kwargs)
29
+
30
+ def exclude(self, *args, **kwargs):
31
+ kwargs = self._convert_location_to_locations(kwargs)
32
+ return super().exclude(*args, **kwargs)
nautobot/ipam/models.py CHANGED
@@ -22,7 +22,7 @@ from nautobot.ipam import choices, constants
22
22
  from nautobot.virtualization.models import VMInterface
23
23
 
24
24
  from .fields import VarbinaryIPField
25
- from .querysets import IPAddressQuerySet, PrefixQuerySet, RIRQuerySet
25
+ from .querysets import IPAddressQuerySet, PrefixQuerySet, RIRQuerySet, VLANQuerySet
26
26
  from .validators import DNSValidator
27
27
 
28
28
  __all__ = (
@@ -1380,6 +1380,7 @@ class VLAN(PrimaryModel):
1380
1380
  ]
1381
1381
 
1382
1382
  natural_key_field_names = ["pk"]
1383
+ objects = BaseManager.from_queryset(VLANQuerySet)()
1383
1384
 
1384
1385
  class Meta:
1385
1386
  ordering = (
@@ -6,6 +6,7 @@ import netaddr
6
6
 
7
7
  from nautobot.core.models.querysets import RestrictedQuerySet
8
8
  from nautobot.core.utils.data import merge_dicts_without_collision
9
+ from nautobot.ipam.mixins import LocationToLocationsQuerySetMixin
9
10
 
10
11
 
11
12
  class RIRQuerySet(RestrictedQuerySet):
@@ -194,7 +195,7 @@ class BaseNetworkQuerySet(RestrictedQuerySet):
194
195
  return ip, last_ip
195
196
 
196
197
 
197
- class PrefixQuerySet(BaseNetworkQuerySet):
198
+ class PrefixQuerySet(LocationToLocationsQuerySetMixin, BaseNetworkQuerySet):
198
199
  """Queryset for `Prefix` objects."""
199
200
 
200
201
  def net_equals(self, *prefixes):
@@ -474,3 +475,7 @@ class IPAddressQuerySet(BaseNetworkQuerySet):
474
475
  q |= Q(pk__in=pk_values)
475
476
 
476
477
  return super().filter(q)
478
+
479
+
480
+ class VLANQuerySet(LocationToLocationsQuerySetMixin, RestrictedQuerySet):
481
+ """Queryset for `VLAN` objects."""
@@ -295,6 +295,50 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
295
295
  self.child1 = Prefix.objects.create(prefix="101.102.0.0/26", status=self.status, namespace=self.namespace)
296
296
  self.child2 = Prefix.objects.create(prefix="101.102.0.64/26", status=self.status, namespace=self.namespace)
297
297
 
298
+ def test_location_queries(self):
299
+ locations = Location.objects.all()[:4]
300
+ for location in locations:
301
+ location.location_type.content_types.add(ContentType.objects.get_for_model(Prefix))
302
+ for i in range(10):
303
+ pfx = Prefix.objects.create(prefix=f"1.1.1.{4*i}/30", status=self.status, namespace=self.namespace)
304
+ if i > 4:
305
+ pfx.locations.set(locations)
306
+
307
+ with self.subTest("Assert filtering and excluding `location`"):
308
+ self.assertQuerysetEqualAndNotEmpty(
309
+ Prefix.objects.filter(location=locations[0]),
310
+ Prefix.objects.filter(locations__in=[locations[0]]),
311
+ )
312
+ self.assertQuerysetEqualAndNotEmpty(
313
+ Prefix.objects.exclude(location=locations[0]),
314
+ Prefix.objects.exclude(locations__in=[locations[0]]),
315
+ )
316
+ self.assertQuerysetEqualAndNotEmpty(
317
+ Prefix.objects.filter(location__in=[locations[0]]),
318
+ Prefix.objects.filter(locations__in=[locations[0]]),
319
+ )
320
+ self.assertQuerysetEqualAndNotEmpty(
321
+ Prefix.objects.exclude(location__in=[locations[0]]),
322
+ Prefix.objects.exclude(locations__in=[locations[0]]),
323
+ )
324
+
325
+ # We use `assertQuerysetEqualAndNotEmpty` for test validation. Including a nullable field could lead
326
+ # to flaky tests where querysets might return None, causing tests to fail. Therefore, we select
327
+ # fields that consistently contain values to ensure reliable filtering.
328
+ query_params = ["name", "location_type", "status"]
329
+
330
+ for field_name in query_params:
331
+ with self.subTest(f"Assert location__{field_name} query."):
332
+ value = getattr(locations[0], field_name)
333
+ self.assertQuerysetEqualAndNotEmpty(
334
+ Prefix.objects.filter(**{f"location__{field_name}": value}),
335
+ Prefix.objects.filter(**{f"locations__{field_name}": value}),
336
+ )
337
+ self.assertQuerysetEqualAndNotEmpty(
338
+ Prefix.objects.exclude(**{f"location__{field_name}": value}),
339
+ Prefix.objects.exclude(**{f"locations__{field_name}": value}),
340
+ )
341
+
298
342
  def test_prefix_validation(self):
299
343
  location_type = LocationType.objects.get(name="Room")
300
344
  location = Location.objects.filter(location_type=location_type).first()
@@ -1201,6 +1245,44 @@ class TestVLAN(ModelTestCases.BaseModelTestCase):
1201
1245
  location.vlans.add(vlan)
1202
1246
  self.assertIn(f"{location} is a Floor and may not have VLANs associated to it.", str(cm.exception))
1203
1247
 
1248
+ def test_location_queries(self):
1249
+ location = VLAN.objects.filter(locations__isnull=False).first().locations.first()
1250
+
1251
+ with self.subTest("Assert filtering and excluding `location`"):
1252
+ self.assertQuerysetEqualAndNotEmpty(
1253
+ VLAN.objects.filter(location=location),
1254
+ VLAN.objects.filter(locations__in=[location]),
1255
+ )
1256
+ self.assertQuerysetEqualAndNotEmpty(
1257
+ VLAN.objects.exclude(location=location),
1258
+ VLAN.objects.exclude(locations__in=[location]),
1259
+ )
1260
+ self.assertQuerysetEqualAndNotEmpty(
1261
+ VLAN.objects.filter(location__in=[location]),
1262
+ VLAN.objects.filter(locations__in=[location]),
1263
+ )
1264
+ self.assertQuerysetEqualAndNotEmpty(
1265
+ VLAN.objects.exclude(location__in=[location]),
1266
+ VLAN.objects.exclude(locations__in=[location]),
1267
+ )
1268
+
1269
+ # We use `assertQuerysetEqualAndNotEmpty` for test validation. Including a nullable field could lead
1270
+ # to flaky tests where querysets might return None, causing tests to fail. Therefore, we select
1271
+ # fields that consistently contain values to ensure reliable filtering.
1272
+ query_params = ["name", "location_type", "status"]
1273
+
1274
+ for field_name in query_params:
1275
+ with self.subTest(f"Assert location__{field_name} query."):
1276
+ value = getattr(location, field_name)
1277
+ self.assertQuerysetEqualAndNotEmpty(
1278
+ VLAN.objects.filter(**{f"location__{field_name}": value}),
1279
+ VLAN.objects.filter(**{f"locations__{field_name}": value}),
1280
+ )
1281
+ self.assertQuerysetEqualAndNotEmpty(
1282
+ VLAN.objects.exclude(**{f"location__{field_name}": value}),
1283
+ VLAN.objects.exclude(**{f"locations__{field_name}": value}),
1284
+ )
1285
+
1204
1286
 
1205
1287
  class TestVRF(ModelTestCases.BaseModelTestCase):
1206
1288
  model = VRF
nautobot/ipam/views.py CHANGED
@@ -332,6 +332,7 @@ class RouteTargetView(generic.ObjectView):
332
332
  return {
333
333
  "importing_vrfs_table": importing_vrfs_table,
334
334
  "exporting_vrfs_table": exporting_vrfs_table,
335
+ **super().get_extra_context(request, instance),
335
336
  }
336
337
 
337
338
 
@@ -389,9 +390,7 @@ class RIRView(generic.ObjectView):
389
390
  }
390
391
  RequestConfig(request, paginate).configure(assigned_prefix_table)
391
392
 
392
- return {
393
- "assigned_prefix_table": assigned_prefix_table,
394
- }
393
+ return {"assigned_prefix_table": assigned_prefix_table, **super().get_extra_context(request, instance)}
395
394
 
396
395
 
397
396
  class RIREditView(generic.ObjectEditView):
@@ -457,6 +456,7 @@ class PrefixView(generic.ObjectView):
457
456
  return {
458
457
  "vrf_table": vrf_table,
459
458
  "parent_prefix_table": parent_prefix_table,
459
+ **super().get_extra_context(request, instance),
460
460
  }
461
461
 
462
462
 
@@ -780,6 +780,7 @@ class IPAddressView(generic.ObjectView):
780
780
  return {
781
781
  "parent_prefixes_table": parent_prefixes_table,
782
782
  "related_ips_table": related_ips_table,
783
+ **super().get_extra_context(request, instance),
783
784
  }
784
785
 
785
786
 
@@ -1261,6 +1262,7 @@ class VLANGroupView(generic.ObjectView):
1261
1262
  "vlan_table": vlan_table,
1262
1263
  "permissions": permissions,
1263
1264
  "vlans_count": vlans_count,
1265
+ **super().get_extra_context(request, instance),
1264
1266
  }
1265
1267
 
1266
1268
 
@@ -1317,9 +1319,7 @@ class VLANView(generic.ObjectView):
1317
1319
  prefix_table = tables.PrefixTable(list(prefixes))
1318
1320
  prefix_table.exclude = ("vlan",)
1319
1321
 
1320
- return {
1321
- "prefix_table": prefix_table,
1322
- }
1322
+ return {"prefix_table": prefix_table, **super().get_extra_context(request, instance)}
1323
1323
 
1324
1324
 
1325
1325
  class VLANInterfacesView(generic.ObjectView):