issues.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import shlex
  2. from datetime import datetime
  3. from typing import Literal, Optional
  4. from django.db.models import Count
  5. from django.db.models.expressions import RawSQL
  6. from django.http import Http404, HttpResponse
  7. from ninja import Field, Query, Schema
  8. from glitchtip.api.authentication import AuthHttpRequest
  9. from glitchtip.api.pagination import apaginate
  10. from ..constants import EventStatus
  11. from ..models import Issue
  12. from ..schema import IssueDetailSchema, IssueSchema
  13. from . import router
  14. def get_queryset(request: AuthHttpRequest, organization_slug: Optional[str] = None):
  15. user_id = request.auth
  16. qs = Issue.objects.filter(project__organization__users=user_id)
  17. if organization_slug:
  18. qs = qs.filter(project__organization__slug=organization_slug)
  19. qs = qs.annotate(
  20. num_comments=Count("comments", distinct=True),
  21. ).select_related("project")
  22. return qs
  23. @router.get(
  24. "/issues/{int:issue_id}/",
  25. response=IssueDetailSchema,
  26. by_alias=True,
  27. )
  28. async def get_issue(request: AuthHttpRequest, issue_id: int):
  29. qs = get_queryset(request)
  30. qs = qs.annotate(
  31. user_report_count=Count("userreport", distinct=True),
  32. )
  33. try:
  34. return await qs.filter(id=issue_id).aget()
  35. except Issue.DoesNotExist:
  36. raise Http404()
  37. class IssueFilters(Schema):
  38. first_seen__gte: datetime = Field(None, alias="start")
  39. first_seen__lte: datetime = Field(None, alias="end")
  40. project__in: list[str] = Field(None, alias="project")
  41. tags__environment__has_any_keys: list[str] = Field(None, alias="environment")
  42. @router.get(
  43. "organizations/{slug:organization_slug}/issues/",
  44. response=list[IssueSchema],
  45. by_alias=True,
  46. )
  47. @apaginate
  48. async def list_issues(
  49. request: AuthHttpRequest,
  50. response: HttpResponse,
  51. organization_slug: str,
  52. filters: Query[IssueFilters],
  53. query: Optional[str] = None,
  54. sort: Literal[
  55. "last_seen",
  56. "first_seen",
  57. "count",
  58. "priority",
  59. "-last_seen",
  60. "-first_seen",
  61. "-count",
  62. "-priority",
  63. ] = "-last_seen",
  64. environment: Optional[list[str]] = None,
  65. ):
  66. qs = get_queryset(request, organization_slug=organization_slug)
  67. if qs_filters := filters.dict(exclude_none=True):
  68. qs = qs.filter(**qs_filters)
  69. if query:
  70. queries = shlex.split(query)
  71. # First look for structured queries
  72. for i, query in enumerate(queries):
  73. query_part = query.split(":", 1)
  74. if len(query_part) == 2:
  75. query_name, query_value = query_part
  76. query_value = query_value.strip('"')
  77. if query_name == "is":
  78. qs = qs.filter(status=EventStatus.from_string(query_value))
  79. elif query_name == "has":
  80. qs = qs.filter(tags__has_key=query_value)
  81. else:
  82. qs = qs.filter(tags__contains={query_name: [query_value]})
  83. if len(query_part) == 1:
  84. search_query = " ".join(queries[i:])
  85. qs = qs.filter(search_vector=search_query)
  86. # Search queries must be at end of query string, finished when parsing
  87. break
  88. if sort.endswith("priority"):
  89. # Raw SQL must be added when sorting by priority
  90. # Inspired by https://stackoverflow.com/a/43788975/443457
  91. qs = qs.annotate(
  92. priority=RawSQL("LOG10(count) + EXTRACT(EPOCH FROM last_seen)/300000", ())
  93. )
  94. return qs.order_by(sort)
  95. # return [obj async for obj in qs]