From 9ad66d7b5057c58a0f844a47cb79eebce0134104 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Thu, 11 Sep 2025 18:01:05 +0200 Subject: [PATCH] API: adhere to Bugsink's DB-transactional model as per https://www.bugsink.com/blog/database-transactions/ --- bugsink/api_mixins.py | 8 ++++++++ events/api_views.py | 3 ++- events/test_api.py | 6 +++--- issues/api_views.py | 4 +++- issues/test_api.py | 6 +++--- projects/api_views.py | 4 ++-- projects/test_api.py | 6 +++--- releases/api_views.py | 3 ++- releases/test_api.py | 6 +++--- teams/api_views.py | 3 ++- teams/test_api.py | 4 ++-- 11 files changed, 33 insertions(+), 20 deletions(-) diff --git a/bugsink/api_mixins.py b/bugsink/api_mixins.py index c240f57..9fc4e36 100644 --- a/bugsink/api_mixins.py +++ b/bugsink/api_mixins.py @@ -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 = {} diff --git a/events/api_views.py b/events/api_views.py index 21cfbdd..3da556d 100644 --- a/events/api_views.py +++ b/events/api_views.py @@ -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= Optional: ?order=asc|desc (default: desc) diff --git a/events/test_api.py b/events/test_api.py index 37d1f40..3cdc3ef 100644 --- a/events/test_api.py +++ b/events/test_api.py @@ -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() diff --git a/issues/api_views.py b/issues/api_views.py index 8f79c96..ecebc38 100644 --- a/issues/api_views.py +++ b/issues/api_views.py @@ -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= Optional: ?order=asc|desc (default: desc) diff --git a/issues/test_api.py b/issues/test_api.py index 68b4b6b..e89b9cb 100644 --- a/issues/test_api.py +++ b/issues/test_api.py @@ -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): diff --git a/projects/api_views.py b/projects/api_views.py index 41a9c6a..86a6235 100644 --- a/projects/api_views.py +++ b/projects/api_views.py @@ -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= filter diff --git a/projects/test_api.py b/projects/test_api.py index c452567..87c86e2 100644 --- a/projects/test_api.py +++ b/projects/test_api.py @@ -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. diff --git a/releases/api_views.py b/releases/api_views.py index 4ffbc33..6648d7f 100644 --- a/releases/api_views.py +++ b/releases/api_views.py @@ -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= Ordered by sort_epoch. diff --git a/releases/test_api.py b/releases/test_api.py index b9de8e2..a609eb5 100644 --- a/releases/test_api.py +++ b/releases/test_api.py @@ -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() diff --git a/teams/api_views.py b/teams/api_views.py index 1e22606..1531fe8 100644 --- a/teams/api_views.py +++ b/teams/api_views.py @@ -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 diff --git a/teams/test_api.py b/teams/test_api.py index e1fcd79..6ee8bc8 100644 --- a/teams/test_api.py +++ b/teams/test_api.py @@ -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()