api.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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 /projects/{organization_slug}/{project_slug}/releases/ (sentry undocumented)
  29. GET /projects/{organization_slug}/{project_slug}/releases/{version}/ (sentry undocumented)
  30. DELETE /projects/{organization_slug}/{project_slug}/releases/{version}/ (sentry undocumented)
  31. POST /projects/{organization_slug}/{project_slug}/releases/ (sentry undocumented)
  32. """
  33. def get_releases_queryset(
  34. organization_slug: str,
  35. user_id: int,
  36. id: Optional[int] = None,
  37. version: Optional[str] = None,
  38. project_slug: Optional[str] = None,
  39. ):
  40. qs = Release.objects.filter(
  41. organization__slug=organization_slug, organization__users=user_id
  42. )
  43. if id:
  44. qs = qs.filter(id=id)
  45. if version:
  46. qs = qs.filter(version=version)
  47. if project_slug:
  48. qs = qs.filter(projects__slug=project_slug)
  49. return qs.prefetch_related("projects")
  50. def get_release_files_queryset(
  51. organization_slug: str,
  52. user_id: int,
  53. version: Optional[str] = None,
  54. project_slug: Optional[str] = None,
  55. ):
  56. qs = ReleaseFile.objects.filter(
  57. release__organization__slug=organization_slug,
  58. release__organization__users=user_id,
  59. )
  60. if version:
  61. qs = qs.filter(release__version=version)
  62. if project_slug:
  63. qs = qs.filter(release__projects__slug=project_slug)
  64. return qs.select_related("file")
  65. @router.post(
  66. "/organizations/{slug:organization_slug}/releases/",
  67. response={201: ReleaseSchema},
  68. by_alias=True,
  69. )
  70. @has_permission(["project:releases"])
  71. async def create_release(
  72. request: AuthHttpRequest, organization_slug: str, payload: ReleaseIn
  73. ):
  74. user_id = request.auth.user_id
  75. organization = await aget_object_or_404(
  76. Organization, slug=organization_slug, users=user_id
  77. )
  78. data = payload.dict()
  79. projects = [
  80. project_id
  81. async for project_id in Project.objects.filter(
  82. slug__in=data.pop("projects"), organization=organization
  83. ).values_list("id", flat=True)
  84. ]
  85. if not projects:
  86. raise ValidationError([{"projects": "Require at least one valid project"}])
  87. release = await Release.objects.acreate(organization=organization, **data)
  88. await release.projects.aadd(*projects)
  89. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  90. @router.post(
  91. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/",
  92. response={201: ReleaseSchema},
  93. by_alias=True,
  94. )
  95. @has_permission(["project:releases"])
  96. async def create_project_release(
  97. request: AuthHttpRequest, organization_slug: str, project_slug, payload: ReleaseBase
  98. ):
  99. user_id = request.auth.user_id
  100. project = await aget_object_or_404(
  101. Project.objects.select_related("organization"),
  102. slug=project_slug,
  103. organization__slug=organization_slug,
  104. organization__users=user_id,
  105. )
  106. data = payload.dict()
  107. version = data.pop("version")
  108. release, _ = await Release.objects.aget_or_create(
  109. organization=project.organization, version=version, defaults=data
  110. )
  111. await release.projects.aadd(project)
  112. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  113. @router.get(
  114. "/organizations/{slug:organization_slug}/releases/",
  115. response=list[ReleaseSchema],
  116. by_alias=True,
  117. )
  118. @paginate
  119. @has_permission(["project:releases"])
  120. async def list_releases(
  121. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  122. ):
  123. return get_releases_queryset(organization_slug, request.auth.user_id)
  124. @router.get(
  125. "/organizations/{slug:organization_slug}/releases/{slug:version}/",
  126. response=ReleaseSchema,
  127. by_alias=True,
  128. )
  129. @has_permission(["project:releases"])
  130. async def get_release(request: AuthHttpRequest, organization_slug: str, version: str):
  131. return await aget_object_or_404(
  132. get_releases_queryset(organization_slug, request.auth.user_id, version=version)
  133. )
  134. @router.put(
  135. "/organizations/{slug:organization_slug}/releases/{slug:version}/",
  136. response=ReleaseSchema,
  137. by_alias=True,
  138. )
  139. @has_permission(["project:releases"])
  140. async def update_release(
  141. request: AuthHttpRequest,
  142. organization_slug: str,
  143. version: str,
  144. payload: ReleaseUpdate,
  145. ):
  146. user_id = request.auth.user_id
  147. release = await aget_object_or_404(
  148. get_releases_queryset(organization_slug, user_id, version=version)
  149. )
  150. for attr, value in payload.dict().items():
  151. setattr(release, attr, value)
  152. await release.asave()
  153. return await get_releases_queryset(organization_slug, user_id, id=release.id).aget()
  154. @router.delete(
  155. "/organizations/{slug:organization_slug}/releases/{slug:version}/",
  156. response={204: None},
  157. )
  158. @has_permission(["project:releases"])
  159. async def delete_organization_release(
  160. request: AuthHttpRequest, organization_slug: str, version: str
  161. ):
  162. result, _ = await get_releases_queryset(
  163. organization_slug, request.auth.user_id, version=version
  164. ).adelete()
  165. if not result:
  166. raise Http404
  167. return 204, None
  168. @router.get(
  169. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/",
  170. response=list[ReleaseSchema],
  171. by_alias=True,
  172. )
  173. @paginate
  174. @has_permission(["project:releases"])
  175. async def list_project_releases(
  176. request: AuthHttpRequest,
  177. response: HttpResponse,
  178. organization_slug: str,
  179. project_slug: str,
  180. ):
  181. return get_releases_queryset(
  182. organization_slug, request.auth.user_id, project_slug=project_slug
  183. )
  184. @router.get(
  185. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/",
  186. response=ReleaseSchema,
  187. by_alias=True,
  188. )
  189. @has_permission(["project:releases"])
  190. async def get_project_release(
  191. request: AuthHttpRequest, organization_slug: str, project_slug: str, version: str
  192. ):
  193. return await aget_object_or_404(
  194. get_releases_queryset(
  195. organization_slug,
  196. request.auth.user_id,
  197. project_slug=project_slug,
  198. version=version,
  199. )
  200. )
  201. @router.delete(
  202. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/",
  203. response={204: None},
  204. )
  205. @has_permission(["project:releases"])
  206. async def delete_project_release(
  207. request: AuthHttpRequest, organization_slug: str, project_slug: str, version: str
  208. ):
  209. result, _ = await get_releases_queryset(
  210. organization_slug,
  211. request.auth.user_id,
  212. version=version,
  213. project_slug=project_slug,
  214. ).adelete()
  215. if not result:
  216. raise Http404
  217. return 204, None
  218. @router.get(
  219. "/projects/{slug:organization_slug}/{slug:project_slug}/releases/{slug:version}/files/",
  220. response=list[ReleaseFileSchema],
  221. by_alias=True,
  222. )
  223. @paginate
  224. @has_permission(["project:releases"])
  225. async def list_project_release_files(
  226. request: AuthHttpRequest,
  227. response: HttpResponse,
  228. organization_slug: str,
  229. project_slug: str,
  230. version: str,
  231. ):
  232. return get_release_files_queryset(
  233. organization_slug,
  234. request.auth.user_id,
  235. project_slug=project_slug,
  236. version=version,
  237. )
  238. @router.get(
  239. "/organizations/{slug:organization_slug}/releases/{slug:version}/files/",
  240. response=list[ReleaseFileSchema],
  241. by_alias=True,
  242. )
  243. @paginate
  244. @has_permission(["project:releases"])
  245. async def list_release_files(
  246. request: AuthHttpRequest,
  247. response: HttpResponse,
  248. organization_slug: str,
  249. version: str,
  250. ):
  251. return get_release_files_queryset(
  252. organization_slug,
  253. request.auth.user_id,
  254. version=version,
  255. )