nautobot 2.4.1__py3-none-any.whl → 2.4.3__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 (461) hide show
  1. nautobot/circuits/templates/circuits/inc/circuit_termination.html +1 -1
  2. nautobot/circuits/tests/integration/test_circuit.py +135 -0
  3. nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +43 -0
  4. nautobot/circuits/tests/integration/test_relationships.py +1 -1
  5. nautobot/circuits/views.py +4 -1
  6. nautobot/cloud/api/views.py +3 -3
  7. nautobot/core/apps/__init__.py +0 -5
  8. nautobot/core/constants.py +0 -1
  9. nautobot/core/forms/__init__.py +2 -0
  10. nautobot/core/forms/forms.py +2 -1
  11. nautobot/core/forms/widgets.py +8 -0
  12. nautobot/core/management/commands/generate_performance_test_endpoints.py +268 -0
  13. nautobot/core/templates/generic/object_bulk_delete.html +1 -1
  14. nautobot/core/templates/generic/object_bulk_destroy.html +1 -1
  15. nautobot/core/templates/generic/object_bulk_edit.html +1 -1
  16. nautobot/core/templates/generic/object_bulk_import.html +1 -1
  17. nautobot/core/templates/generic/object_create.html +5 -0
  18. nautobot/core/templates/generic/object_delete.html +1 -1
  19. nautobot/core/templates/generic/object_detail.html +1 -1
  20. nautobot/core/templates/generic/object_edit.html +1 -1
  21. nautobot/core/templates/inc/javascript.html +2 -0
  22. nautobot/core/templates/widgets/clearable_file.html +5 -0
  23. nautobot/core/templatetags/helpers.py +3 -3
  24. nautobot/core/testing/integration.py +469 -12
  25. nautobot/core/tests/test_commands.py +31 -0
  26. nautobot/core/tests/test_jobs.py +34 -2
  27. nautobot/core/tests/test_utils.py +17 -2
  28. nautobot/core/utils/git.py +7 -2
  29. nautobot/core/utils/lookup.py +12 -1
  30. nautobot/core/views/generic.py +10 -2
  31. nautobot/core/views/mixins.py +22 -7
  32. nautobot/core/views/utils.py +2 -2
  33. nautobot/dcim/api/views.py +11 -10
  34. nautobot/dcim/forms.py +15 -6
  35. nautobot/dcim/models/devices.py +1 -2
  36. nautobot/dcim/tables/devices.py +2 -1
  37. nautobot/dcim/templates/dcim/cable.html +1 -1
  38. nautobot/dcim/templates/dcim/cable_trace.html +4 -4
  39. nautobot/dcim/templates/dcim/consoleport.html +14 -4
  40. nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
  41. nautobot/dcim/templates/dcim/device/base.html +1 -1
  42. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
  43. nautobot/dcim/templates/dcim/device.html +2 -2
  44. nautobot/dcim/templates/dcim/device_component.html +1 -1
  45. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  46. nautobot/dcim/templates/dcim/frontport.html +7 -2
  47. nautobot/dcim/templates/dcim/interface.html +9 -4
  48. nautobot/dcim/templates/dcim/location.html +1 -1
  49. nautobot/dcim/templates/dcim/locationtype.html +1 -1
  50. nautobot/dcim/templates/dcim/locationtype_retrieve.html +1 -1
  51. nautobot/dcim/templates/dcim/manufacturer.html +1 -1
  52. nautobot/dcim/templates/dcim/platform.html +1 -1
  53. nautobot/dcim/templates/dcim/powerfeed.html +9 -4
  54. nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
  55. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  56. nautobot/dcim/templates/dcim/powerport.html +14 -4
  57. nautobot/dcim/templates/dcim/rack.html +1 -1
  58. nautobot/dcim/templates/dcim/rackgroup.html +1 -1
  59. nautobot/dcim/templates/dcim/rackreservation.html +2 -2
  60. nautobot/dcim/templates/dcim/rearport.html +7 -2
  61. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  62. nautobot/dcim/tests/integration/test_device_bulk_operations.py +30 -0
  63. nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
  64. nautobot/dcim/tests/integration/test_location_bulk_operations.py +43 -0
  65. nautobot/dcim/tests/test_models.py +1 -1
  66. nautobot/dcim/tests/test_views.py +9 -1
  67. nautobot/dcim/views.py +12 -15
  68. nautobot/extras/api/serializers.py +33 -0
  69. nautobot/extras/api/views.py +13 -5
  70. nautobot/extras/constants.py +1 -0
  71. nautobot/extras/datasources/git.py +125 -0
  72. nautobot/extras/forms/forms.py +4 -0
  73. nautobot/extras/jobs.py +8 -1
  74. nautobot/extras/migrations/0122_add_graphqlquery_owner_content_type.py +34 -0
  75. nautobot/extras/models/customfields.py +29 -12
  76. nautobot/extras/models/datasources.py +85 -0
  77. nautobot/extras/models/models.py +15 -0
  78. nautobot/extras/models/relationships.py +17 -5
  79. nautobot/extras/signals.py +15 -1
  80. nautobot/extras/templates/extras/computedfield.html +1 -1
  81. nautobot/extras/templates/extras/configcontext.html +1 -1
  82. nautobot/extras/templates/extras/configcontextschema.html +1 -1
  83. nautobot/extras/templates/extras/customfield.html +1 -1
  84. nautobot/extras/templates/extras/customlink.html +1 -1
  85. nautobot/extras/templates/extras/dynamicgroup.html +1 -1
  86. nautobot/extras/templates/extras/exporttemplate.html +1 -1
  87. nautobot/extras/templates/extras/gitrepository.html +1 -1
  88. nautobot/extras/templates/extras/graphqlquery.html +1 -1
  89. nautobot/extras/templates/extras/job.html +1 -0
  90. nautobot/extras/templates/extras/job_detail.html +1 -1
  91. nautobot/extras/templates/extras/jobbutton_retrieve.html +1 -1
  92. nautobot/extras/templates/extras/jobhook.html +1 -1
  93. nautobot/extras/templates/extras/jobresult.html +1 -1
  94. nautobot/extras/templates/extras/objectchange.html +1 -1
  95. nautobot/extras/templates/extras/plugin_detail.html +1 -1
  96. nautobot/extras/templates/extras/relationship.html +1 -63
  97. nautobot/extras/templates/extras/role_retrieve.html +1 -1
  98. nautobot/extras/templates/extras/scheduledjob.html +1 -1
  99. nautobot/extras/templates/extras/secret.html +1 -1
  100. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  101. nautobot/extras/templates/extras/status.html +1 -1
  102. nautobot/extras/templates/extras/tag.html +1 -1
  103. nautobot/extras/templates/extras/webhook.html +1 -1
  104. nautobot/extras/tests/git_data/01-valid-files/graphql_queries/device_interfaces.gql +8 -0
  105. nautobot/extras/tests/git_data/01-valid-files/graphql_queries/device_names.gql +5 -0
  106. nautobot/extras/tests/git_data/02-invalid-files/graphql_queries/bad_device_names.gql +5 -0
  107. nautobot/extras/tests/git_helper.py +9 -1
  108. nautobot/extras/tests/integration/__init__.py +29 -16
  109. nautobot/extras/tests/test_api.py +6 -0
  110. nautobot/extras/tests/test_customfields.py +49 -51
  111. nautobot/extras/tests/test_datasources.py +27 -0
  112. nautobot/extras/tests/test_dynamicgroups.py +14 -0
  113. nautobot/extras/tests/test_models.py +283 -0
  114. nautobot/extras/tests/test_utils.py +22 -1
  115. nautobot/extras/tests/test_views.py +197 -9
  116. nautobot/extras/utils.py +47 -8
  117. nautobot/extras/views.py +84 -26
  118. nautobot/ipam/api/views.py +3 -3
  119. nautobot/ipam/forms.py +2 -6
  120. nautobot/ipam/models.py +8 -2
  121. nautobot/ipam/tables.py +2 -2
  122. nautobot/ipam/templates/ipam/ipaddress.html +1 -1
  123. nautobot/ipam/templates/ipam/prefix.html +1 -1
  124. nautobot/ipam/templates/ipam/rir.html +1 -1
  125. nautobot/ipam/templates/ipam/routetarget.html +1 -1
  126. nautobot/ipam/templates/ipam/service.html +1 -1
  127. nautobot/ipam/templates/ipam/vlan.html +1 -1
  128. nautobot/ipam/templates/ipam/vlangroup.html +1 -1
  129. nautobot/ipam/templates/ipam/vrf.html +1 -1
  130. nautobot/ipam/tests/test_models.py +24 -0
  131. nautobot/ipam/tests/test_utils.py +41 -2
  132. nautobot/ipam/utils/__init__.py +18 -11
  133. nautobot/project-static/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
  134. nautobot/project-static/docs/404.html +87 -12
  135. nautobot/project-static/docs/apps/index.html +88 -13
  136. nautobot/project-static/docs/apps/nautobot-apps.html +88 -13
  137. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js → bundle.60a45f97.min.js} +1 -1
  138. nautobot/project-static/docs/assets/javascripts/{bundle.88dd0f4e.min.js.map → bundle.60a45f97.min.js.map} +1 -1
  139. nautobot/project-static/docs/assets/javascripts/workers/{search.6ce7567c.min.js → search.f8cc74c7.min.js} +1 -1
  140. nautobot/project-static/docs/assets/javascripts/workers/{search.6ce7567c.min.js.map → search.f8cc74c7.min.js.map} +1 -1
  141. nautobot/project-static/docs/assets/stylesheets/{main.6f8fc17f.min.css → main.a40c8224.min.css} +1 -1
  142. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +87 -12
  143. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +87 -12
  144. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +87 -12
  145. nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +87 -12
  146. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +87 -12
  147. nautobot/project-static/docs/code-reference/nautobot/apps/config.html +87 -12
  148. nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +87 -12
  149. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +87 -12
  150. nautobot/project-static/docs/code-reference/nautobot/apps/events.html +87 -12
  151. nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +87 -12
  152. nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +87 -12
  153. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +87 -12
  154. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +87 -12
  155. nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +87 -12
  156. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +87 -12
  157. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +87 -12
  158. nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +87 -12
  159. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +87 -12
  160. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +87 -12
  161. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +87 -12
  162. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +87 -12
  163. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +87 -12
  164. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +177 -20
  165. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +114 -17
  166. nautobot/project-static/docs/development/apps/api/configuration-view.html +87 -12
  167. nautobot/project-static/docs/development/apps/api/database-backend-config.html +87 -12
  168. nautobot/project-static/docs/development/apps/api/models/django-admin.html +87 -12
  169. nautobot/project-static/docs/development/apps/api/models/global-search.html +87 -12
  170. nautobot/project-static/docs/development/apps/api/models/graphql.html +96 -21
  171. nautobot/project-static/docs/development/apps/api/models/index.html +87 -12
  172. nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +87 -12
  173. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +87 -12
  174. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +89 -14
  175. nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +87 -12
  176. nautobot/project-static/docs/development/apps/api/platform-features/index.html +87 -12
  177. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +87 -12
  178. nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +87 -12
  179. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +87 -12
  180. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +87 -12
  181. nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +87 -12
  182. nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +87 -12
  183. nautobot/project-static/docs/development/apps/api/prometheus.html +87 -12
  184. nautobot/project-static/docs/development/apps/api/setup.html +88 -13
  185. nautobot/project-static/docs/development/apps/api/testing.html +87 -12
  186. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +87 -12
  187. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +87 -12
  188. nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +87 -12
  189. nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +87 -12
  190. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +87 -12
  191. nautobot/project-static/docs/development/apps/api/views/base-template.html +87 -12
  192. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +87 -12
  193. nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +87 -12
  194. nautobot/project-static/docs/development/apps/api/views/help-documentation.html +87 -12
  195. nautobot/project-static/docs/development/apps/api/views/index.html +87 -12
  196. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +87 -12
  197. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +87 -12
  198. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +87 -12
  199. nautobot/project-static/docs/development/apps/api/views/notes.html +87 -12
  200. nautobot/project-static/docs/development/apps/api/views/rest-api.html +87 -12
  201. nautobot/project-static/docs/development/apps/api/views/urls.html +87 -12
  202. nautobot/project-static/docs/development/apps/index.html +87 -12
  203. nautobot/project-static/docs/development/apps/migration/code-updates.html +93 -17
  204. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +89 -14
  205. nautobot/project-static/docs/development/apps/migration/from-v1.html +90 -15
  206. nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +87 -12
  207. nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +87 -12
  208. nautobot/project-static/docs/development/apps/migration/model-updates/global.html +87 -12
  209. nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +87 -12
  210. nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +87 -12
  211. nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +87 -12
  212. nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +88 -13
  213. nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +87 -12
  214. nautobot/project-static/docs/development/apps/porting-from-netbox.html +87 -12
  215. nautobot/project-static/docs/development/core/application-registry.html +87 -12
  216. nautobot/project-static/docs/development/core/best-practices.html +88 -13
  217. nautobot/project-static/docs/development/core/bootstrap-ui.html +88 -13
  218. nautobot/project-static/docs/development/core/caching.html +87 -12
  219. nautobot/project-static/docs/development/core/controllers.html +87 -12
  220. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +94 -19
  221. nautobot/project-static/docs/development/core/generic-views.html +87 -12
  222. nautobot/project-static/docs/development/core/getting-started.html +89 -14
  223. nautobot/project-static/docs/development/core/homepage.html +87 -12
  224. nautobot/project-static/docs/development/core/index.html +88 -13
  225. nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +90 -15
  226. nautobot/project-static/docs/development/core/model-checklist.html +88 -13
  227. nautobot/project-static/docs/development/core/model-features.html +87 -12
  228. nautobot/project-static/docs/development/core/natural-keys.html +87 -12
  229. nautobot/project-static/docs/development/core/navigation-menu.html +88 -13
  230. nautobot/project-static/docs/development/core/release-checklist.html +88 -13
  231. nautobot/project-static/docs/development/core/role-internals.html +87 -12
  232. nautobot/project-static/docs/development/core/settings.html +88 -13
  233. nautobot/project-static/docs/development/core/style-guide.html +91 -16
  234. nautobot/project-static/docs/development/core/templates.html +88 -13
  235. nautobot/project-static/docs/development/core/testing.html +87 -12
  236. nautobot/project-static/docs/development/core/ui-component-framework.html +87 -12
  237. nautobot/project-static/docs/development/core/user-preferences.html +87 -12
  238. nautobot/project-static/docs/development/index.html +87 -12
  239. nautobot/project-static/docs/development/jobs/index.html +95 -13
  240. nautobot/project-static/docs/development/jobs/migration/from-v1.html +90 -14
  241. nautobot/project-static/docs/index.html +90 -14
  242. nautobot/project-static/docs/objects.inv +0 -0
  243. nautobot/project-static/docs/overview/application_stack.html +89 -14
  244. nautobot/project-static/docs/overview/design_philosophy.html +87 -12
  245. nautobot/project-static/docs/release-notes/index.html +87 -12
  246. nautobot/project-static/docs/release-notes/version-1.0.html +89 -14
  247. nautobot/project-static/docs/release-notes/version-1.1.html +89 -14
  248. nautobot/project-static/docs/release-notes/version-1.2.html +90 -15
  249. nautobot/project-static/docs/release-notes/version-1.3.html +88 -13
  250. nautobot/project-static/docs/release-notes/version-1.4.html +104 -29
  251. nautobot/project-static/docs/release-notes/version-1.5.html +95 -20
  252. nautobot/project-static/docs/release-notes/version-1.6.html +91 -16
  253. nautobot/project-static/docs/release-notes/version-2.0.html +97 -22
  254. nautobot/project-static/docs/release-notes/version-2.1.html +94 -19
  255. nautobot/project-static/docs/release-notes/version-2.2.html +88 -13
  256. nautobot/project-static/docs/release-notes/version-2.3.html +91 -16
  257. nautobot/project-static/docs/release-notes/version-2.4.html +465 -12
  258. nautobot/project-static/docs/requirements.txt +1 -1
  259. nautobot/project-static/docs/search/search_index.json +1 -1
  260. nautobot/project-static/docs/sitemap.xml +296 -288
  261. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  262. nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +90 -15
  263. nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +87 -12
  264. nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +91 -16
  265. nautobot/project-static/docs/user-guide/administration/configuration/index.html +87 -12
  266. nautobot/project-static/docs/user-guide/administration/configuration/redis.html +88 -13
  267. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +90 -15
  268. nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +87 -12
  269. nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +95 -20
  270. nautobot/project-static/docs/user-guide/administration/guides/docker.html +90 -15
  271. nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +88 -13
  272. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +87 -12
  273. nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +91 -16
  274. nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +87 -12
  275. nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +102 -27
  276. nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +89 -14
  277. nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +87 -12
  278. nautobot/project-static/docs/user-guide/administration/installation/app-install.html +88 -13
  279. nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +87 -12
  280. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +87 -12
  281. nautobot/project-static/docs/user-guide/administration/installation/index.html +87 -12
  282. nautobot/project-static/docs/user-guide/administration/installation/install_system.html +88 -13
  283. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +93 -18
  284. nautobot/project-static/docs/user-guide/administration/installation/services.html +88 -13
  285. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +87 -12
  286. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +87 -12
  287. nautobot/project-static/docs/user-guide/administration/security/index.html +9420 -0
  288. nautobot/project-static/docs/user-guide/administration/security/notices.html +9844 -0
  289. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +87 -12
  290. nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +88 -13
  291. nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +87 -12
  292. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +87 -12
  293. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +87 -12
  294. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +87 -12
  295. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +87 -12
  296. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +87 -12
  297. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +87 -12
  298. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +98 -20
  299. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +87 -12
  300. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +87 -12
  301. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +87 -12
  302. nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +87 -12
  303. nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +87 -12
  304. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +87 -12
  305. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +87 -12
  306. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +87 -12
  307. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +87 -12
  308. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +87 -12
  309. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +87 -12
  310. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +87 -12
  311. nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +87 -12
  312. nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +87 -12
  313. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +87 -12
  314. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +87 -12
  315. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +87 -12
  316. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +87 -12
  317. nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +87 -12
  318. nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +87 -12
  319. nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +87 -12
  320. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +87 -12
  321. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +87 -12
  322. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +87 -12
  323. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +87 -12
  324. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +87 -12
  325. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +87 -12
  326. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +87 -12
  327. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +87 -12
  328. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +87 -12
  329. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +87 -12
  330. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +87 -12
  331. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +87 -12
  332. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +87 -12
  333. nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +87 -12
  334. nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +87 -12
  335. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +87 -12
  336. nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +87 -12
  337. nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +87 -12
  338. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +87 -12
  339. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +87 -12
  340. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +87 -12
  341. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +87 -12
  342. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +87 -12
  343. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +87 -12
  344. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +87 -12
  345. nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +87 -12
  346. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +87 -12
  347. nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +87 -12
  348. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +87 -12
  349. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +87 -12
  350. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +87 -12
  351. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +87 -12
  352. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +87 -12
  353. nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +87 -12
  354. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +87 -12
  355. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +87 -12
  356. nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +87 -12
  357. nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +87 -12
  358. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +87 -12
  359. nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +87 -12
  360. nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +87 -12
  361. nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +87 -12
  362. nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +87 -12
  363. nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +87 -12
  364. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +87 -12
  365. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +87 -12
  366. nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +87 -12
  367. nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +87 -12
  368. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +87 -12
  369. nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +87 -12
  370. nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +87 -12
  371. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +87 -12
  372. nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +87 -12
  373. nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +87 -12
  374. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +87 -12
  375. nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +87 -12
  376. nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +87 -12
  377. nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +87 -12
  378. nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +87 -12
  379. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +87 -12
  380. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +99 -24
  381. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +87 -12
  382. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +87 -12
  383. nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +90 -15
  384. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +87 -12
  385. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +87 -12
  386. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +87 -12
  387. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +87 -12
  388. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +87 -12
  389. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +87 -12
  390. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +188 -30
  391. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +87 -12
  392. nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +87 -12
  393. nautobot/project-static/docs/user-guide/feature-guides/relationships.html +87 -12
  394. nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +87 -12
  395. nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +87 -12
  396. nautobot/project-static/docs/user-guide/index.html +87 -12
  397. nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +87 -12
  398. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +87 -12
  399. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +87 -12
  400. nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +87 -12
  401. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +88 -13
  402. nautobot/project-static/docs/user-guide/platform-functionality/events.html +87 -12
  403. nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +87 -12
  404. nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +87 -12
  405. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +407 -14
  406. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +90 -15
  407. nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +87 -12
  408. nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +87 -12
  409. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +87 -12
  410. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +87 -12
  411. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +87 -12
  412. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +87 -12
  413. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +89 -14
  414. nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +93 -18
  415. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +87 -12
  416. nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +87 -12
  417. nautobot/project-static/docs/user-guide/platform-functionality/note.html +87 -12
  418. nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +87 -12
  419. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +87 -12
  420. nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +87 -12
  421. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +87 -12
  422. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +87 -12
  423. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +87 -12
  424. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +87 -12
  425. nautobot/project-static/docs/user-guide/platform-functionality/role.html +87 -12
  426. nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +87 -12
  427. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +87 -12
  428. nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +87 -12
  429. nautobot/project-static/docs/user-guide/platform-functionality/status.html +87 -12
  430. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +87 -12
  431. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +87 -12
  432. nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +87 -12
  433. nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +87 -12
  434. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +87 -12
  435. nautobot/project-static/js/dropdown.js +28 -0
  436. nautobot/tenancy/forms.py +9 -0
  437. nautobot/tenancy/templates/tenancy/tenant.html +1 -2
  438. nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
  439. nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
  440. nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
  441. nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
  442. nautobot/tenancy/tests/test_views.py +5 -1
  443. nautobot/tenancy/urls.py +7 -79
  444. nautobot/tenancy/views.py +51 -80
  445. nautobot/virtualization/templates/virtualization/cluster.html +1 -1
  446. nautobot/virtualization/templates/virtualization/clustergroup.html +1 -1
  447. nautobot/virtualization/templates/virtualization/clustertype.html +1 -1
  448. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  449. nautobot/virtualization/templates/virtualization/vminterface.html +1 -1
  450. nautobot/wireless/api/serializers.py +6 -1
  451. nautobot/wireless/api/views.py +3 -3
  452. nautobot/wireless/tests/test_api.py +5 -0
  453. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/METADATA +12 -12
  454. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/RECORD +459 -443
  455. nautobot/dcim/tests/integration/test_device_bulk_delete.py +0 -189
  456. nautobot/dcim/tests/integration/test_device_bulk_edit.py +0 -181
  457. /nautobot/project-static/docs/assets/stylesheets/{main.6f8fc17f.min.css.map → main.a40c8224.min.css.map} +0 -0
  458. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/LICENSE.txt +0 -0
  459. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/NOTICE +0 -0
  460. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/WHEEL +0 -0
  461. {nautobot-2.4.1.dist-info → nautobot-2.4.3.dist-info}/entry_points.txt +0 -0
@@ -30,6 +30,7 @@ from nautobot.extras.models import (
30
30
  DynamicGroup,
31
31
  ExportTemplate,
32
32
  GitRepository,
33
+ GraphQLQuery,
33
34
  Job,
34
35
  JobQueue,
35
36
  JobResult,
@@ -935,6 +936,120 @@ def delete_git_export_templates(repository_record, job_result, preserve=None):
935
936
  job_result.log(msg, level_choice=LogLevelChoices.LOG_WARNING, grouping="export templates")
936
937
 
937
938
 
939
+ #
940
+ # GraphQL handling
941
+ #
942
+
943
+
944
+ def refresh_git_graphql_queries(repository_record, job_result, delete=False):
945
+ """Callback function for GitRepository updates - refresh all GraphQLQuery managed by this repository."""
946
+ if "extras.graphqlquery" in repository_record.provided_contents and not delete:
947
+ update_git_graphql_queries(repository_record, job_result)
948
+ else:
949
+ delete_git_graphql_queries(repository_record, job_result)
950
+
951
+
952
+ logger = logging.getLogger(__name__)
953
+
954
+
955
+ def update_git_graphql_queries(repository_record, job_result):
956
+ """Refresh any GraphQL queries provided by this Git repository."""
957
+ graphql_query_path = os.path.join(repository_record.filesystem_path, "graphql_queries")
958
+ git_repository_content_type = ContentType.objects.get_for_model(GitRepository)
959
+ graphql_queries = []
960
+
961
+ if os.path.isdir(graphql_query_path):
962
+ for file in os.listdir(graphql_query_path):
963
+ file_path = os.path.join(graphql_query_path, file)
964
+ if not os.path.isfile(file_path):
965
+ continue
966
+
967
+ # Remove `.gql` extension from the name if it exists
968
+ query_name = file.rsplit(".gql", 1)[0] if file.endswith(".gql") else file
969
+
970
+ try:
971
+ with open(file_path, "r") as fd:
972
+ query_content = fd.read().strip()
973
+
974
+ graphql_query, created = GraphQLQuery.objects.get_or_create(
975
+ name=query_name,
976
+ owner_content_type=git_repository_content_type,
977
+ owner_object_id=repository_record.pk,
978
+ defaults={"query": query_content},
979
+ )
980
+ modified = graphql_query.query != query_content
981
+ graphql_queries.append(query_name)
982
+ # Only attempt to update if the content has changed
983
+ if modified:
984
+ try:
985
+ graphql_query.query = query_content
986
+ graphql_query.validated_save()
987
+ msg = (
988
+ f"Successfully created GraphQL query: {query_name}"
989
+ if created
990
+ else f"Successfully updated GraphQL query: {query_name}"
991
+ )
992
+ logger.info(msg)
993
+ job_result.log(
994
+ msg, obj=graphql_query, level_choice=LogLevelChoices.LOG_INFO, grouping="graphql queries"
995
+ )
996
+ except Exception as exc:
997
+ # Log validation error and retain the existing query
998
+ error_msg = (
999
+ f"Invalid GraphQL syntax for query '{query_name}'. "
1000
+ f"Retaining the existing query. Error: {exc}"
1001
+ )
1002
+ logger.error(error_msg)
1003
+ job_result.log(error_msg, level_choice=LogLevelChoices.LOG_ERROR, grouping="graphql queries")
1004
+ continue
1005
+ else:
1006
+ msg = f"No changes to GraphQL query: {query_name}"
1007
+ logger.info(msg)
1008
+ job_result.log(
1009
+ msg, obj=graphql_query, level_choice=LogLevelChoices.LOG_INFO, grouping="graphql queries"
1010
+ )
1011
+
1012
+ except Exception as exc:
1013
+ # Check if a query with the same name already exists
1014
+ existing_query = GraphQLQuery.objects.filter(name=query_name).first()
1015
+ if existing_query and existing_query.owner_object_id != repository_record.pk:
1016
+ error_msg = (
1017
+ f"GraphQL query '{query_name}' already exists "
1018
+ f"Please rename the query in the repository and try again."
1019
+ )
1020
+ else:
1021
+ error_msg = f"Error processing GraphQL query file '{file}': {exc}"
1022
+
1023
+ # Log the error
1024
+ logger.error(error_msg)
1025
+ job_result.log(error_msg, level_choice=LogLevelChoices.LOG_ERROR, grouping="graphql queries")
1026
+
1027
+ # Delete any queries not in the preserved list
1028
+ delete_git_graphql_queries(repository_record, job_result, preserve=graphql_queries)
1029
+
1030
+
1031
+ def delete_git_graphql_queries(repository_record, job_result, preserve=None):
1032
+ """Delete GraphQL queries owned by the given Git repository that are not in the preserve list."""
1033
+ git_repository_content_type = ContentType.objects.get_for_model(GitRepository)
1034
+ if preserve is None:
1035
+ preserve = []
1036
+
1037
+ for graphql_query in GraphQLQuery.objects.filter(
1038
+ owner_content_type=git_repository_content_type,
1039
+ owner_object_id=repository_record.pk,
1040
+ ):
1041
+ if graphql_query.name not in preserve:
1042
+ try:
1043
+ graphql_query.delete()
1044
+ msg = f"Deleted GraphQL query: {graphql_query.name}"
1045
+ logger.warning(msg)
1046
+ job_result.log(msg, level_choice=LogLevelChoices.LOG_WARNING, grouping="graphql queries")
1047
+ except Exception as exc:
1048
+ error_msg = f"Unable to delete '{graphql_query.name}': {exc}"
1049
+ logger.error(error_msg)
1050
+ job_result.log(error_msg, level_choice=LogLevelChoices.LOG_ERROR, grouping="graphql queries")
1051
+
1052
+
938
1053
  # Register built-in callbacks for data types potentially provided by a GitRepository
939
1054
  register_datasource_contents(
940
1055
  [
@@ -978,5 +1093,15 @@ register_datasource_contents(
978
1093
  callback=refresh_git_export_templates,
979
1094
  ),
980
1095
  ),
1096
+ (
1097
+ "extras.gitrepository",
1098
+ DatasourceContent(
1099
+ name="graphql queries",
1100
+ content_identifier="extras.graphqlquery",
1101
+ icon="mdi-graphql",
1102
+ weight=400,
1103
+ callback=refresh_git_graphql_queries,
1104
+ ),
1105
+ ),
981
1106
  ]
982
1107
  )
@@ -38,6 +38,7 @@ from nautobot.core.forms import (
38
38
  )
39
39
  from nautobot.core.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
40
40
  from nautobot.core.forms.forms import ConfirmationForm
41
+ from nautobot.core.forms.widgets import ClearableFileInput
41
42
  from nautobot.core.utils.deprecation import class_deprecated_in_favor_of
42
43
  from nautobot.dcim.models import Device, DeviceRedundancyGroup, DeviceType, Location, Platform
43
44
  from nautobot.extras.choices import (
@@ -985,6 +986,9 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
985
986
  "name",
986
987
  "image",
987
988
  ]
989
+ widgets = {
990
+ "image": ClearableFileInput,
991
+ }
988
992
 
989
993
 
990
994
  #
nautobot/extras/jobs.py CHANGED
@@ -37,6 +37,7 @@ from nautobot.core.forms import (
37
37
  DynamicModelMultipleChoiceField,
38
38
  JSONField,
39
39
  )
40
+ from nautobot.core.forms.widgets import ClearableFileInput
40
41
  from nautobot.core.utils.config import get_settings_or_config
41
42
  from nautobot.core.utils.logging import sanitize
42
43
  from nautobot.core.utils.lookup import get_model_from_name
@@ -1040,12 +1041,18 @@ class DatabaseFileField(forms.FileField):
1040
1041
  widget = DBClearableFileInput
1041
1042
 
1042
1043
 
1044
+ class BootstrapStyleFileField(forms.FileField):
1045
+ """File picker with UX bootstrap style and clearable checkbox."""
1046
+
1047
+ widget = ClearableFileInput
1048
+
1049
+
1043
1050
  class FileVar(ScriptVariable):
1044
1051
  """
1045
1052
  An uploaded file.
1046
1053
  """
1047
1054
 
1048
- form_field = DatabaseFileField
1055
+ form_field = BootstrapStyleFileField
1049
1056
 
1050
1057
 
1051
1058
  class IPAddressVar(ScriptVariable):
@@ -0,0 +1,34 @@
1
+ # Generated by Django 4.2.17 on 2025-01-08 16:59
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+ import nautobot.extras.utils
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+ dependencies = [
11
+ ("contenttypes", "0002_remove_content_type_name"),
12
+ ("extras", "0121_alter_team_contacts"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name="graphqlquery",
18
+ name="owner_content_type",
19
+ field=models.ForeignKey(
20
+ blank=True,
21
+ default=None,
22
+ limit_choices_to=nautobot.extras.utils.FeatureQuery("graphql_query_owners"),
23
+ null=True,
24
+ on_delete=django.db.models.deletion.CASCADE,
25
+ related_name="graphql_queries",
26
+ to="contenttypes.contenttype",
27
+ ),
28
+ ),
29
+ migrations.AddField(
30
+ model_name="graphqlquery",
31
+ name="owner_object_id",
32
+ field=models.UUIDField(blank=True, default=None, null=True),
33
+ ),
34
+ ]
@@ -370,11 +370,11 @@ class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
370
370
 
371
371
  def get_for_model(self, model, exclude_filter_disabled=False):
372
372
  """
373
- Return all CustomFields assigned to the given model.
373
+ Return (and cache) all CustomFields assigned to the given model.
374
374
 
375
375
  Args:
376
- model: The django model to which custom fields are registered
377
- exclude_filter_disabled: Exclude any custom fields which have filter logic disabled
376
+ model (Model): The django model to which custom fields are registered
377
+ exclude_filter_disabled (bool): Exclude any custom fields which have filter logic disabled
378
378
  """
379
379
  concrete_model = model._meta.concrete_model
380
380
  cache_key = (
@@ -521,6 +521,26 @@ class CustomField(
521
521
  def __str__(self):
522
522
  return self.label
523
523
 
524
+ @property
525
+ def choices_cache_key(self):
526
+ return f"nautobot.extras.customfield.choices.{self.pk}"
527
+
528
+ @property
529
+ def choices(self) -> list[str]:
530
+ """
531
+ Cacheable shorthand for retrieving custom_field_choices values associated with this model.
532
+
533
+ Returns:
534
+ list[str]: List of choice values, ordered by weight.
535
+ """
536
+ if self.type not in [CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT]:
537
+ return []
538
+ choices = cache.get(self.choices_cache_key)
539
+ if choices is None:
540
+ choices = list(self.custom_field_choices.order_by("weight", "value").values_list("value", flat=True))
541
+ cache.set(self.choices_cache_key, choices)
542
+ return choices
543
+
524
544
  def save(self, *args, **kwargs):
525
545
  self.clean()
526
546
  super().save(*args, **kwargs)
@@ -679,12 +699,11 @@ class CustomField(
679
699
 
680
700
  # Select or Multi-select
681
701
  else:
682
- choices = [(cfc.value, cfc.value) for cfc in self.custom_field_choices.all()]
683
- default_choice = self.custom_field_choices.filter(value=self.default).first()
702
+ choices = [(value, value) for value in self.choices]
684
703
 
685
704
  # Set the initial value to the first available choice (if any)
686
705
  if self.type == CustomFieldTypeChoices.TYPE_SELECT and not for_filter_form:
687
- if not required or default_choice is None:
706
+ if not required or self.default not in self.choices:
688
707
  choices = add_blank_choice(choices)
689
708
  field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
690
709
  field = field_class(
@@ -779,17 +798,15 @@ class CustomField(
779
798
 
780
799
  # Validate selected choice
781
800
  elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
782
- if value not in self.custom_field_choices.values_list("value", flat=True):
783
- raise ValidationError(
784
- f"Invalid choice ({value}). Available choices are: {', '.join(self.custom_field_choices.values_list('value', flat=True))}"
785
- )
801
+ if value not in self.choices:
802
+ raise ValidationError(f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}")
786
803
 
787
804
  elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
788
805
  if isinstance(value, str):
789
806
  value = value.split(",")
790
- if not set(value).issubset(self.custom_field_choices.values_list("value", flat=True)):
807
+ if not set(value).issubset(self.choices):
791
808
  raise ValidationError(
792
- f"Invalid choice(s) ({value}). Available choices are: {', '.join(self.custom_field_choices.values_list('value', flat=True))}"
809
+ f"Invalid choice(s) ({value}). Available choices are: {', '.join(self.choices)}"
793
810
  )
794
811
 
795
812
  elif self.required:
@@ -1,7 +1,11 @@
1
1
  """Models for representing external data sources."""
2
2
 
3
+ from contextlib import contextmanager
3
4
  from importlib.util import find_spec
5
+ import logging
4
6
  import os
7
+ import shutil
8
+ import tempfile
5
9
 
6
10
  from django.conf import settings
7
11
  from django.core.exceptions import ValidationError
@@ -12,12 +16,16 @@ from nautobot.core.constants import CHARFIELD_MAX_LENGTH
12
16
  from nautobot.core.models.fields import AutoSlugField, LaxURLField, slugify_dashes_to_underscores
13
17
  from nautobot.core.models.generics import PrimaryModel
14
18
  from nautobot.core.models.validators import EnhancedURLValidator
19
+ from nautobot.core.utils.git import GitRepo
15
20
  from nautobot.extras.utils import check_if_key_is_graphql_safe, extras_features
16
21
 
22
+ logger = logging.getLogger(__name__)
23
+
17
24
 
18
25
  @extras_features(
19
26
  "config_context_owners",
20
27
  "export_template_owners",
28
+ "graphql_query_owners",
21
29
  "graphql",
22
30
  "job_results",
23
31
  "webhooks",
@@ -167,3 +175,80 @@ class GitRepository(PrimaryModel):
167
175
  if dry_run:
168
176
  return enqueue_git_repository_diff_origin_and_local(self, user)
169
177
  return enqueue_pull_git_repository_and_refresh_data(self, user)
178
+
179
+ @contextmanager
180
+ def clone_to_directory_context(self, path=None, branch=None, head=None, depth=0):
181
+ """
182
+ Context manager to perform a (shallow or full) clone of the Git repository in a temporary directory.
183
+
184
+ Args:
185
+ path (str, optional): The absolute directory path to clone into. If not specified, `tempfile.gettempdir()` will be used.
186
+ branch (str, optional): The branch to checkout. If not set, the GitRepository.branch will be used.
187
+ head (str, optional): Git commit hash to check out instead of pulling branch latest.
188
+ depth (int, optional): The depth of the clone. If set to 0, a full clone will be performed.
189
+
190
+ Returns:
191
+ Returns the absolute path of the cloned repo if clone was successful, otherwise returns None.
192
+ """
193
+
194
+ if branch and head:
195
+ raise ValueError("Cannot specify both branch and head")
196
+
197
+ path_name = None
198
+ try:
199
+ path_name = self.clone_to_directory(path=path, branch=branch, head=head, depth=depth)
200
+ yield path_name
201
+ finally:
202
+ # Cleanup the temporary directory
203
+ if path_name:
204
+ self.cleanup_cloned_directory(path_name)
205
+
206
+ def clone_to_directory(self, path=None, branch=None, head=None, depth=0):
207
+ """
208
+ Perform a (shallow or full) clone of the Git repository in a temporary directory.
209
+
210
+ Args:
211
+ path (str, optional): The absolute directory path to clone into. If not specified, `tempfile.gettempdir()` will be used.
212
+ branch (str, optional): The branch to checkout. If not set, the GitRepository.branch will be used.
213
+ head (str, optional): Git commit hash to check out instead of pulling branch latest.
214
+ depth (int, optional): The depth of the clone. If set to 0, a full clone will be performed.
215
+
216
+ Returns:
217
+ Returns the absolute path of the cloned repo if clone was successful, otherwise returns None.
218
+ """
219
+ if branch and head:
220
+ raise ValueError("Cannot specify both branch and head")
221
+
222
+ try:
223
+ path_name = tempfile.mkdtemp(dir=path, prefix=self.slug)
224
+ except PermissionError as e:
225
+ logger.error(f"Failed to create temporary directory at {path}: {e}")
226
+ raise e
227
+
228
+ if not branch:
229
+ branch = self.branch
230
+
231
+ try:
232
+ repo_helper = GitRepo(path_name, self.remote_url, depth=depth, branch=branch)
233
+ if head:
234
+ repo_helper.checkout(branch, head)
235
+ except Exception as e:
236
+ logger.error(f"Failed to clone repository {self.name} to {path_name}: {e}")
237
+ raise e
238
+
239
+ logger.info(f"Cloned repository {self.name} to {path_name}")
240
+ return path_name
241
+
242
+ def cleanup_cloned_directory(self, path):
243
+ """
244
+ Cleanup the cloned directory.
245
+
246
+ Args:
247
+ path (str): The absolute directory path to cleanup.
248
+ """
249
+
250
+ try:
251
+ shutil.rmtree(path)
252
+ except OSError as os_error:
253
+ # log error if the cleanup fails
254
+ logger.error(f"Failed to cleanup temporary directory at {path}: {os_error}")
@@ -680,6 +680,21 @@ class GraphQLQuery(
680
680
  SavedViewMixin,
681
681
  BaseModel,
682
682
  ):
683
+ # A Graphql Query *may* be owned by another model, such as a GitRepository, or it may be un-owned
684
+ owner_content_type = models.ForeignKey(
685
+ to=ContentType,
686
+ on_delete=models.CASCADE,
687
+ limit_choices_to=FeatureQuery("graphql_query_owners"),
688
+ default=None,
689
+ null=True,
690
+ blank=True,
691
+ related_name="graphql_queries",
692
+ )
693
+ owner_object_id = models.UUIDField(default=None, null=True, blank=True)
694
+ owner = GenericForeignKey(
695
+ ct_field="owner_content_type",
696
+ fk_field="owner_object_id",
697
+ )
683
698
  name = models.CharField(max_length=CHARFIELD_MAX_LENGTH, unique=True)
684
699
  query = models.TextField()
685
700
  variables = models.JSONField(encoder=DjangoJSONEncoder, default=dict, blank=True)
@@ -261,15 +261,27 @@ class RelationshipModel(models.Model):
261
261
  remote_model = remote_ct.model_class()
262
262
  if remote_model is not None:
263
263
  if not relationship.symmetric:
264
- query_params = {f"{peer_side}_for_associations__relationship": relationship.pk}
265
- resp[side][relationship] = remote_model.objects.filter(**query_params)
264
+ query_params = {
265
+ f"{peer_side}_for_associations__relationship": relationship,
266
+ f"{peer_side}_for_associations__{side}_id": self.pk,
267
+ }
268
+ # Get the related objects for this relationship on the opposite side.
269
+ resp[side][relationship] = remote_model.objects.filter(**query_params).distinct()
266
270
  if not relationship.has_many(peer_side):
267
271
  resp[side][relationship] = resp[side][relationship].first()
268
272
  else:
273
+ side_query_params = {
274
+ f"{peer_side}_for_associations__relationship": relationship,
275
+ f"{peer_side}_for_associations__{side}_id": self.pk,
276
+ }
277
+ peer_side_query_params = {
278
+ f"{side}_for_associations__relationship": relationship,
279
+ f"{side}_for_associations__{peer_side}_id": self.pk,
280
+ }
281
+ # Get the related objects based on the pks we gathered.
269
282
  resp[RelationshipSideChoices.SIDE_PEER][relationship] = remote_model.objects.filter(
270
- Q(source_for_associations__relationship=relationship.pk)
271
- | Q(destination_for_associations__relationship=relationship.pk)
272
- ).exclude(pk=self.pk)
283
+ Q(**side_query_params) | Q(**peer_side_query_params)
284
+ ).distinct()
273
285
  if not relationship.has_many(peer_side):
274
286
  resp[side][relationship] = resp[side][relationship].first()
275
287
  else:
@@ -28,6 +28,7 @@ from nautobot.extras.models import (
28
28
  ComputedField,
29
29
  ContactAssociation,
30
30
  CustomField,
31
+ CustomFieldChoice,
31
32
  DynamicGroup,
32
33
  DynamicGroupMembership,
33
34
  GitRepository,
@@ -97,6 +98,19 @@ def invalidate_models_cache(sender, **kwargs):
97
98
  cache.delete_pattern(f"{manager.keys_for_model.cache_key_prefix}.*")
98
99
 
99
100
 
101
+ @receiver(post_delete, sender=CustomField)
102
+ @receiver(post_delete, sender=CustomFieldChoice)
103
+ @receiver(post_save, sender=CustomFieldChoice)
104
+ @receiver(post_save, sender=CustomField)
105
+ def invalidate_choices_cache(sender, instance, **kwargs):
106
+ """Invalidate the choices cache for CustomFields."""
107
+ with contextlib.suppress(redis.exceptions.ConnectionError):
108
+ if sender is CustomField:
109
+ cache.delete(instance.choices_cache_key)
110
+ else:
111
+ cache.delete(instance.custom_field.choices_cache_key)
112
+
113
+
100
114
  @receiver(post_save, sender=Relationship)
101
115
  @receiver(m2m_changed, sender=Relationship)
102
116
  @receiver(post_delete, sender=Relationship)
@@ -376,7 +390,7 @@ def git_repository_pre_delete(instance, **kwargs):
376
390
  app.control.broadcast("discard_git_repository", repository_slug=instance.slug)
377
391
  # But we don't have an equivalent way to broadcast to any other Django instances.
378
392
  # For now we just delete the one that we have locally and rely on other methods,
379
- # such as the import_jobs() signal that runs on server startup,
393
+ # such as the import_jobs() signal that runs on post migrate,
380
394
  # to clean up other clones as they're encountered.
381
395
  if os.path.isdir(instance.filesystem_path):
382
396
  shutil.rmtree(instance.filesystem_path)
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load buttons %}
3
3
  {% load plugins %}
4
4
  {% load helpers %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load buttons %}
3
3
  {% load helpers %}
4
4
 
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block extra_nav_tabs %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block extra_buttons %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -192,4 +192,5 @@
192
192
  toggleExecutionType();
193
193
  });
194
194
  </script>
195
+ {{ job_form.media }}
195
196
  {% endblock %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
  {% load perms %}
4
4
  {% load plugins %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block content_left_page %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
  {% load custom_links %}
4
4
  {% load form_helpers %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block title %}{{ object }}{% endblock %}
@@ -1,4 +1,4 @@
1
- {% extends 'generic/object_detail.html' %}
1
+ {% extends 'generic/object_retrieve.html' %}
2
2
  {% load helpers %}
3
3
 
4
4
  {% block header %}