api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. from typing import Optional
  2. from django.http import Http404, HttpResponse
  3. from django.shortcuts import aget_object_or_404
  4. from ninja import Router
  5. from ninja.errors import ValidationError
  6. from apps.organizations_ext.models import Organization
  7. from apps.projects.models import Project
  8. from glitchtip.api.authentication import AuthHttpRequest
  9. from glitchtip.api.pagination import paginate
  10. from glitchtip.api.permissions import has_permission
  11. from .models import Release, ReleaseFile
  12. from .schema import (
  13. ReleaseBase,
  14. ReleaseFileSchema,
  15. ReleaseIn,
  16. ReleaseSchema,
  17. ReleaseUpdate,
  18. )
  19. router = Router()
  20. """
  21. POST /organizations/{organization_slug}/releases/
  22. POST /organizations/{organization_slug}/releases/{version}/deploys/ (Not implemented)
  23. GET /organizations/{organization_slug}/releases/
  24. GET /organizations/{organization_slug}/releases/{version}/
  25. PUT /organizations/{organization_slug}/releases/{version}/
  26. DELETE /organizations/{organization_slug}/releases/{version}/
  27. GET /organizations/{organization_slug}/releases/{version}/files/
  28. GET /organizations/{organization_slug}/releases/{version}/files/{file_id}/
  29. DELETE /organizations/{organization_slug}/releases/{version}/files/{file_id}/
  30. GET /projects/{organization_slug}/{project_slug}/releases/ (sentry undocumented)
  31. GET /projects/{organization_slug}/{project_slug}/releases/{version}/ (sentry undocumented)
  32. DELETE /projects/{organization_slug}/{project_slug}/releases/{version}/ (sentry undocumented)
  33. POST /projects/{organization_slug}/{project_slug}/releases/ (sentry undocumented)
  34. GET /projects/{organization_slug}/{project_slug}/releases/{version}/files/{file_id}/
  35. DELETE /projects/{organization_slug}/{project_slug}/releases/{version}/files/{file_id}/ (sentry undocumented)
  36. """
  37. def get_releases_queryset(
  38. organization_slug: str,
  39. user_id: int,
  40. id: Optional[int] = None,
  41. version: Optional[str] = None,
  42. project_slug: Optional[str] = None,
  43. ):
  44. qs = Release.objects.filter(
  45. organization__slug=organization_slug, organization__users=user_id
  46. )
  47. if id:
  48. qs = qs.filter(id=id)
  49. if version:
  50. qs = qs.filter(version=version)
  51. if project_slug:
  52. qs = qs.filter(projects__slug=project_slug)
  53. return qs.prefetch_related("projects")
  54. def get_release_files_queryset(
  55. organization_slug: str,
  56. user_id: int,
  57. version: Optional[str] = None,
  58. project_slug: Optional[str] = None,
  59. id: Optional[int] = None,
  60. ):
  61. qs = ReleaseFile.objects.filter(
  62. release__organization__slug=organization_slug,
  63. release__organization__users=user_id,
  64. )
  65. if id:
  66. qs = qs.filter(id=id)
  67. if version:
  68. qs = qs.filter(release__version=version)
  69. if project_slug:
  70. qs = qs.filter(release__projects__slug=project_slug)
  71. return qs.select_related("file")
  72. @router.post(
  73. "/organizations/{slug:organization_slug}/releases/",
  74. response={201: ReleaseSchema},
  75. by_alias=True,
  76. )
  77. @has_permission(["project:releases"])
  78. async def create_release(
  79. request: AuthHttpRequest, organization_slug: str, payload: ReleaseIn
  80. ):
  81. user_id = request.auth.user_id
  82. organization = await aget_object_or_404(
  83. Organization, slug=organization_slug, users=user_id
  84. )
  85. data = payload.dict()
  86. projects = [
  87. project_id
  88. async for project_id in Project.objects.filter(
  89. slug__in=data.pop("projects"), organization=organization
  90. ).values_list("id", flat=True)
  91. ]
  92. if not projects:
  93. raise ValidationError([{"projects": "Require at least one valid project"}])
  94. release = await Release.objects.acreate(organization=organization, **data)
  95. await release.projects.aadd(*projects)
  96. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  97. @router.post(
  98. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/",
  99. response={201: ReleaseSchema},
  100. by_alias=True,
  101. )
  102. @has_permission(["project:releases"])
  103. async def create_project_release(
  104. request: AuthHttpRequest, organization_slug: str, project_slug, payload: ReleaseBase
  105. ):
  106. user_id = request.auth.user_id
  107. project = await aget_object_or_404(
  108. Project.objects.select_related("organization"),
  109. slug=project_slug,
  110. organization__slug=organization_slug,
  111. organization__users=user_id,
  112. )
  113. data = payload.dict()
  114. version = data.pop("version")
  115. release, _ = await Release.objects.aget_or_create(
  116. organization=project.organization, version=version, defaults=data
  117. )
  118. await release.projects.aadd(project)
  119. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  120. @router.get(
  121. "/organizations/{slug:organization_slug}/releases/",
  122. response=list[ReleaseSchema],
  123. by_alias=True,
  124. )
  125. @paginate
  126. @has_permission(["project:releases"])
  127. async def list_releases(
  128. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  129. ):
  130. return get_releases_queryset(organization_slug, request.auth.user_id)
  131. @router.get(
  132. "/organizations/{slug:organization_slug}/releases/{slug:version}/",
  133. response=ReleaseSchema,
  134. by_alias=True,
  135. )
  136. @has_permission(["project:releases"])
  137. async def get_release(request: AuthHttpRequest, organization_slug: str, version: str):
  138. return await aget_object_or_404(
  139. get_releases_queryset(organization_slug, request.auth.user_id, version=version)
  140. )
  141. @router.put(
  142. "/organizations/{slug:organization_slug}/releases/{slug:version}/",
  143. response=ReleaseSchema,
  144. by_alias=True,
  145. )
  146. @has_permission(["project:releases"])
  147. async def update_release(
  148. request: AuthHttpRequest,
  149. organization_slug: str,
  150. version: str,
  151. payload: ReleaseUpdate,
  152. ):
  153. user_id = request.auth.user_id
  154. release = await aget_object_or_404(
  155. get_releases_queryset(organization_slug, user_id, version=version)
  156. )
  157. for attr, value in payload.dict().items():
  158. setattr(release, attr, value)
  159. await release.asave()
  160. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  161. @router.delete(
  162. "/organizations/{slug:organization_slug}/releases/{slug:version}/",
  163. response={204: None},
  164. )
  165. @has_permission(["project:releases"])
  166. async def delete_organization_release(
  167. request: AuthHttpRequest, organization_slug: str, version: str
  168. ):
  169. result, _ = await get_releases_queryset(
  170. organization_slug, request.auth.user_id, version=version
  171. ).adelete()
  172. if not result:
  173. raise Http404
  174. return 204, None
  175. @router.get(
  176. "/organizations/{slug:organization_slug}/releases/{slug:version}/files/",
  177. response=list[ReleaseFileSchema],
  178. by_alias=True,
  179. )
  180. @paginate
  181. @has_permission(["project:releases"])
  182. async def list_release_files(
  183. request: AuthHttpRequest,
  184. response: HttpResponse,
  185. organization_slug: str,
  186. version: str,
  187. ):
  188. return get_release_files_queryset(
  189. organization_slug,
  190. request.auth.user_id,
  191. version=version,
  192. )
  193. @router.get(
  194. "/organizations/{slug:organization_slug}/releases/{slug:version}/files/{int:file_id}/",
  195. response=ReleaseFileSchema,
  196. by_alias=True,
  197. )
  198. @has_permission(["project:releases"])
  199. async def get_organization_release_file(
  200. request: AuthHttpRequest,
  201. organization_slug: str,
  202. project_slug: str,
  203. version: str,
  204. file_id: int,
  205. ):
  206. return await aget_object_or_404(
  207. get_release_files_queryset(
  208. organization_slug,
  209. request.auth.user_id,
  210. project_slug=project_slug,
  211. version=version,
  212. id=file_id,
  213. )
  214. )
  215. @router.delete(
  216. "/organizations/{slug:organization_slug}/releases/{slug:version}/files/{int:file_id}/",
  217. response={204: None},
  218. )
  219. @has_permission(["project:releases"])
  220. async def delete_organization_release_file(
  221. request: AuthHttpRequest, organization_slug: str, version: str, file_id: int
  222. ):
  223. result, _ = await get_release_files_queryset(
  224. organization_slug, request.auth.user_id, version=version, id=file_id
  225. ).adelete()
  226. if not result:
  227. raise Http404
  228. return 204, None
  229. @router.get(
  230. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/",
  231. response=list[ReleaseSchema],
  232. by_alias=True,
  233. )
  234. @paginate
  235. @has_permission(["project:releases"])
  236. async def list_project_releases(
  237. request: AuthHttpRequest,
  238. response: HttpResponse,
  239. organization_slug: str,
  240. project_slug: str,
  241. ):
  242. return get_releases_queryset(
  243. organization_slug, request.auth.user_id, project_slug=project_slug
  244. )
  245. @router.get(
  246. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/",
  247. response=ReleaseSchema,
  248. by_alias=True,
  249. )
  250. @has_permission(["project:releases"])
  251. async def get_project_release(
  252. request: AuthHttpRequest, organization_slug: str, project_slug: str, version: str
  253. ):
  254. return await aget_object_or_404(
  255. get_releases_queryset(
  256. organization_slug,
  257. request.auth.user_id,
  258. project_slug=project_slug,
  259. version=version,
  260. )
  261. )
  262. @router.delete(
  263. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/",
  264. response={204: None},
  265. )
  266. @has_permission(["project:releases"])
  267. async def delete_project_release(
  268. request: AuthHttpRequest, organization_slug: str, project_slug: str, version: str
  269. ):
  270. result, _ = await get_releases_queryset(
  271. organization_slug,
  272. request.auth.user_id,
  273. version=version,
  274. project_slug=project_slug,
  275. ).adelete()
  276. if not result:
  277. raise Http404
  278. return 204, None
  279. @router.get(
  280. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/files/",
  281. response=list[ReleaseFileSchema],
  282. by_alias=True,
  283. )
  284. @paginate
  285. @has_permission(["project:releases"])
  286. async def list_project_release_files(
  287. request: AuthHttpRequest,
  288. response: HttpResponse,
  289. organization_slug: str,
  290. project_slug: str,
  291. version: str,
  292. ):
  293. return get_release_files_queryset(
  294. organization_slug,
  295. request.auth.user_id,
  296. project_slug=project_slug,
  297. version=version,
  298. )
  299. @router.delete(
  300. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/files/{int:file_id}/",
  301. response={204: None},
  302. )
  303. @has_permission(["project:releases"])
  304. async def delete_project_release_file(
  305. request: AuthHttpRequest,
  306. organization_slug: str,
  307. project_slug: str,
  308. version: str,
  309. file_id: int,
  310. ):
  311. result, _ = await get_release_files_queryset(
  312. organization_slug,
  313. request.auth.user_id,
  314. version=version,
  315. id=file_id,
  316. project_slug=project_slug,
  317. ).adelete()
  318. if not result:
  319. raise Http404
  320. return 204, None
  321. @router.get(
  322. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/files/{int:file_id}/",
  323. response=ReleaseFileSchema,
  324. by_alias=True,
  325. )
  326. @has_permission(["project:releases"])
  327. async def get_project_release_file(
  328. request: AuthHttpRequest,
  329. organization_slug: str,
  330. project_slug: str,
  331. version: str,
  332. file_id: int,
  333. ):
  334. return await aget_object_or_404(
  335. get_release_files_queryset(
  336. organization_slug,
  337. request.auth.user_id,
  338. project_slug=project_slug,
  339. version=version,
  340. id=file_id,
  341. )
  342. )