Issues API

See #146
This commit is contained in:
Klaas van Schelven
2025-09-09 22:08:30 +02:00
parent 4844c72415
commit c4c749d1e4
2 changed files with 125 additions and 1 deletions

View File

@@ -1,13 +1,61 @@
from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from rest_framework.exceptions import ValidationError
from .models import Issue, Grouping
from .serializers import IssueSerializer, GroupingSerializer
class IssueViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Issue.objects.all().order_by('digest_order') # TBD
"""
LIST requires: ?project=<uuid>
Optional: ?order=asc|desc (default: desc)
LIST ordered by last_seen
RETRIEVE is a pure PK lookup (soft-deletes implied)
"""
queryset = Issue.objects.filter(is_deleted=False) # hide soft-deleted issues; also satisfies router
serializer_class = IssueSerializer
def get_queryset(self):
return self.queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
if self.action != "list":
return queryset
query_params = self.request.query_params
project = query_params.get("project")
if not project:
# the below until we have a UI for cross-project Issue listing, i.e. #190
raise ValidationError({"project": ["This field is required."]})
order = query_params.get("order", "desc")
if order not in ("asc", "desc"):
raise ValidationError({"order": ["Must be 'asc' or 'desc'."]})
ordering = "last_seen" if order == "asc" else "-last_seen"
return queryset.filter(project=project).order_by(ordering)
def get_object(self):
"""
DRF's get_object(), but bypass filter_queryset for detail.
"""
# TODO: copy/paste from events/api_views.py
queryset = self.get_queryset()
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument named "%s".'
% (self.__class__.__name__, lookup_url_kwarg)
)
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
self.check_object_permissions(self.request, obj)
return obj
class GroupingViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Grouping.objects.all().order_by('grouping_key') # TBD

76
issues/test_api.py Normal file
View File

@@ -0,0 +1,76 @@
from django.test import TestCase as DjangoTestCase
from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APIClient
from bsmain.models import AuthToken
from projects.models import Project
from issues.models import Issue
from issues.factories import get_or_create_issue
from events.factories import create_event_data
class IssueApiTests(DjangoTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token.token}")
self.project = Project.objects.create(name="Test Project")
# create two distinct issues for ordering tests (different grouping keys)
data0 = create_event_data(exception_type="E0")
data1 = create_event_data(exception_type="E1")
self.issue0, _ = get_or_create_issue(project=self.project, event_data=data0)
self.issue1, _ = get_or_create_issue(project=self.project, event_data=data1)
# ensure deterministic last_seen ordering
now = timezone.now()
Issue.objects.filter(id=self.issue0.id).update(last_seen=now)
Issue.objects.filter(id=self.issue1.id).update(last_seen=now + timezone.timedelta(seconds=1))
self.issue0.refresh_from_db()
self.issue1.refresh_from_db()
def test_list_requires_project(self):
response = self.client.get(reverse("api:issue-list"))
self.assertEqual(response.status_code, 400)
self.assertEqual({"project": ["This field is required."]}, response.json())
def test_list_by_project_default_desc(self):
response = self.client.get(reverse("api:issue-list"), {"project": str(self.project.id)})
self.assertEqual(response.status_code, 200)
ids = [row["id"] for row in response.json()["results"]]
self.assertEqual(ids[0], str(self.issue1.id))
self.assertEqual(ids[1], str(self.issue0.id))
def test_list_by_project_order_asc(self):
response = self.client.get(reverse("api:issue-list"), {"project": str(self.project.id), "order": "asc"})
self.assertEqual(response.status_code, 200)
ids = [row["id"] for row in response.json()["results"]]
self.assertEqual(ids[0], str(self.issue0.id))
self.assertEqual(ids[1], str(self.issue1.id))
def test_list_rejects_bad_order(self):
response = self.client.get(reverse("api:issue-list"), {"project": str(self.project.id), "order": "sideways"})
self.assertEqual(response.status_code, 400)
self.assertEqual({"order": ["Must be 'asc' or 'desc'."]}, response.json())
def test_detail_by_id(self):
url = reverse("api:issue-detail", args=[self.issue0.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["id"], str(self.issue0.id))
def test_detail_ignores_query_filters(self):
url = reverse("api:issue-detail", args=[self.issue0.id])
response = self.client.get(url, {"project": "00000000-0000-0000-0000-000000000000", "order": "asc"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["id"], str(self.issue0.id))
def test_detail_404_on_is_deleted(self):
Issue.objects.filter(id=self.issue0.id).update(is_deleted=True)
url = reverse("api:issue-detail", args=[self.issue0.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 404)