API: adhere to Bugsink's DB-transactional model

as per https://www.bugsink.com/blog/database-transactions/
This commit is contained in:
Klaas van Schelven
2025-09-11 18:01:05 +02:00
parent 1c0745f24f
commit 9ad66d7b50
11 changed files with 33 additions and 20 deletions

View File

@@ -1,5 +1,13 @@
from rest_framework.exceptions import ValidationError
from bugsink.decorators import atomic_for_request_method
class AtomicRequestMixin:
def dispatch(self, request, *args, **kwargs):
wrapped = atomic_for_request_method(super().dispatch, using=None)
return wrapped(request, *args, **kwargs)
class ExpandableSerializerMixin:
expandable_fields = {}

View File

@@ -4,6 +4,7 @@ from rest_framework.exceptions import ValidationError
from bugsink.utils import assert_
from bugsink.api_pagination import AscDescCursorPagination
from bugsink.api_mixins import AtomicRequestMixin
from .models import Event
from .serializers import EventListSerializer, EventDetailSerializer
@@ -18,7 +19,7 @@ class EventPagination(AscDescCursorPagination):
default_direction = "desc" # newest first by default, aligned with UI
class EventViewSet(viewsets.ReadOnlyModelViewSet):
class EventViewSet(AtomicRequestMixin, viewsets.ReadOnlyModelViewSet):
"""
LIST requires: ?issue=<uuid>
Optional: ?order=asc|desc (default: desc)

View File

@@ -1,4 +1,4 @@
from django.test import TestCase as DjangoTestCase
from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase
from django.urls import reverse
from rest_framework.test import APIClient
@@ -11,7 +11,7 @@ from issues.factories import get_or_create_issue
from events.factories import create_event_data
class EventApiTests(DjangoTestCase):
class EventApiTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()
@@ -70,7 +70,7 @@ class EventApiTests(DjangoTestCase):
self.assertEqual(ids[1], str(e1.id))
class EventPaginationTests(DjangoTestCase):
class EventPaginationTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()

View File

@@ -3,6 +3,8 @@ from rest_framework import viewsets
from rest_framework.pagination import CursorPagination
from rest_framework.exceptions import ValidationError
from bugsink.api_mixins import AtomicRequestMixin
from .models import Issue
from .serializers import IssueSerializer
@@ -53,7 +55,7 @@ class IssuesCursorPagination(CursorPagination):
return ["last_seen", "id"]
class IssueViewSet(viewsets.ReadOnlyModelViewSet):
class IssueViewSet(AtomicRequestMixin, viewsets.ReadOnlyModelViewSet):
"""
LIST requires: ?project=<uuid>
Optional: ?order=asc|desc (default: desc)

View File

@@ -1,4 +1,4 @@
from django.test import TestCase as DjangoTestCase
from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase
from django.urls import reverse
from django.utils import timezone
@@ -13,7 +13,7 @@ from events.factories import create_event_data
from issues.api_views import IssueViewSet
class IssueApiTests(DjangoTestCase):
class IssueApiTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()
@@ -86,7 +86,7 @@ class IssueApiTests(DjangoTestCase):
self.assertEqual(r.json(), {"sort": ["Must be 'digest_order' or 'last_seen'."]})
class IssuePaginationTests(DjangoTestCase):
class IssuePaginationTests(TransactionTestCase):
last_seen_deltas = [3, 1, 4, 0, 2]
def setUp(self):

View File

@@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from bugsink.api_pagination import AscDescCursorPagination
from bugsink.api_mixins import ExpandViewSetMixin
from bugsink.api_mixins import ExpandViewSetMixin, AtomicRequestMixin
from .models import Project
from .serializers import (
@@ -20,7 +20,7 @@ class ProjectPagination(AscDescCursorPagination):
default_direction = "asc"
class ProjectViewSet(ExpandViewSetMixin, viewsets.ModelViewSet):
class ProjectViewSet(AtomicRequestMixin, ExpandViewSetMixin, viewsets.ModelViewSet):
"""
/api/canonical/0/projects/
GET /projects/ → list ordered by name ASC, hides soft-deleted, optional ?team=<uuid> filter

View File

@@ -1,4 +1,4 @@
from django.test import TestCase as DjangoTestCase
from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase
from django.urls import reverse
from rest_framework.test import APIClient
@@ -7,7 +7,7 @@ from teams.models import Team
from projects.models import Project
class ProjectApiTests(DjangoTestCase):
class ProjectApiTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()
@@ -77,7 +77,7 @@ class ProjectApiTests(DjangoTestCase):
self.assertEqual(r.status_code, 405)
class ExpansionTests(DjangoTestCase):
class ExpansionTests(TransactionTestCase):
"""
Expansion tests are exercised via ProjectViewSet, but the intent is to validate the
generic ExpandableSerializerMixin infrastructure.

View File

@@ -2,6 +2,7 @@ from rest_framework import viewsets
from rest_framework.exceptions import ValidationError
from bugsink.api_pagination import AscDescCursorPagination
from bugsink.api_mixins import AtomicRequestMixin
from .models import Release
from .serializers import ReleaseListSerializer, ReleaseDetailSerializer, ReleaseCreateSerializer
@@ -16,7 +17,7 @@ class ReleasePagination(AscDescCursorPagination):
default_direction = "desc"
class ReleaseViewSet(viewsets.ModelViewSet):
class ReleaseViewSet(AtomicRequestMixin, viewsets.ModelViewSet):
"""
LIST requires: ?project=<id>
Ordered by sort_epoch.

View File

@@ -1,4 +1,4 @@
from django.test import TestCase as DjangoTestCase
from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase
from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APIClient
@@ -9,7 +9,7 @@ from releases.models import Release
from releases.api_views import ReleaseViewSet
class ReleaseApiTests(DjangoTestCase):
class ReleaseApiTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()
@@ -81,7 +81,7 @@ class ReleaseApiTests(DjangoTestCase):
self.assertEqual(delete_response.status_code, 405)
class ReleasePaginationTests(DjangoTestCase):
class ReleasePaginationTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()

View File

@@ -2,6 +2,7 @@ from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from bugsink.api_pagination import AscDescCursorPagination
from bugsink.api_mixins import AtomicRequestMixin
from .models import Team
from .serializers import (
@@ -19,7 +20,7 @@ class TeamPagination(AscDescCursorPagination):
default_direction = "asc"
class TeamViewSet(viewsets.ModelViewSet):
class TeamViewSet(AtomicRequestMixin, viewsets.ModelViewSet):
"""
/api/canonical/0/teams/
GET /teams/ → list ordered by name ASC

View File

@@ -1,4 +1,4 @@
from django.test import TestCase as DjangoTestCase
from bugsink.test_utils import TransactionTestCase25251 as TransactionTestCase
from django.urls import reverse
from rest_framework.test import APIClient
@@ -6,7 +6,7 @@ from bsmain.models import AuthToken
from teams.models import Team
class TeamApiTests(DjangoTestCase):
class TeamApiTests(TransactionTestCase):
def setUp(self):
self.client = APIClient()
token = AuthToken.objects.create()