Features: 1) Introduced extend_schema for multiple viewsets to improve OpenAPI documentation; 2) Added detailed schema definitions for blog and payments viewsets using drf-spectacular; 3) Transitioned download_digital_asset functionality to class-based DownloadDigitalAssetView for better modularity.

Fixes: 1) Standardized error responses in `DownloadDigitalAssetView`.

Extra: Improved maintainability by refactoring serializers and schema definitions into modular components; updated API URLs to use new class-based view.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-10-26 18:44:19 +03:00
parent c9fd4b4f98
commit c0fcde4bb4
8 changed files with 323 additions and 65 deletions

17
blog/docs/drf/viewsets.py Normal file
View file

@ -0,0 +1,17 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework import status
from core.docs.drf import BASE_ERRORS
from blog.serializers import PostSerializer
POST_SCHEMA = {
"list": extend_schema(
summary=_("list all posts (read-only)"),
responses={status.HTTP_200_OK: PostSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single post (read-only)"),
responses={status.HTTP_200_OK: PostSerializer(), **BASE_ERRORS},
),
}

View file

@ -1,28 +1,30 @@
from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema_view
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from blog.filters import PostFilter from blog.filters import PostFilter
from blog.models import Post from blog.models import Post
from blog.serializers import PostSerializer from blog.serializers import PostSerializer
from blog.docs.drf.viewsets import POST_SCHEMA
from core.permissions import EvibesPermission from core.permissions import EvibesPermission
@extend_schema_view(**POST_SCHEMA)
class PostViewSet(ReadOnlyModelViewSet): # type: ignore [type-arg] class PostViewSet(ReadOnlyModelViewSet): # type: ignore [type-arg]
""" __doc__ = _( # type: ignore [assignment]
Encapsulates operations for managing and retrieving Post entities in a read-only model view set. "Encapsulates operations for managing and retrieving Post entities in a read-only model view set.\n\n"
"This class is tailored to handle Post objects that are active and allows filtration based on defined "
This class is tailored to handle Post objects that are active and allows filtration based on defined "filters. It integrates with Django's backend filtering system and ensures operations align with the "
filters. It integrates with Django's backend filtering system and ensures operations align with the "defined permissions. The view set also includes an additional 'retrieve' permission configuration.\n\n"
defined permissions. The view set also includes an additional "retrieve" permission configuration. "Attributes:\n"
" serializer_class: Specifies the serializer to be used for Post objects.\n"
Attributes: " permission_classes: Defines the permissions required to interact with this view set.\n"
serializer_class: Specifies the serializer to be used for Post objects. " queryset: Determines the initial queryset, filtered to include only active Post objects.\n"
permission_classes: Defines the permissions required to interact with this view set. " filter_backends: Lists the backends to be used for filtering querysets.\n"
queryset: Determines the initial queryset, filtered to include only active Post objects. " filterset_class: Defines the set of filters used for filtering Post objects.\n"
filter_backends: Lists the backends to be used for filtering querysets. " additional: Contains additional configuration, such as specific action permissions."
filterset_class: Defines the set of filters used for filtering Post objects. )
additional: Contains additional configuration, such as specific action permissions.
"""
serializer_class = PostSerializer serializer_class = PostSerializer
permission_classes = (EvibesPermission,) permission_classes = (EvibesPermission,)

View file

@ -5,11 +5,11 @@ from core.sitemaps import BrandSitemap, CategorySitemap, ProductSitemap, StaticP
from core.views import ( from core.views import (
CacheOperatorView, CacheOperatorView,
ContactUsView, ContactUsView,
DownloadDigitalAssetView,
GlobalSearchView, GlobalSearchView,
RequestCursedURLView, RequestCursedURLView,
SupportedLanguagesView, SupportedLanguagesView,
WebsiteParametersView, WebsiteParametersView,
download_digital_asset_view,
sitemap_detail, sitemap_detail,
sitemap_index, sitemap_index,
) )
@ -78,7 +78,7 @@ urlpatterns = [
{"sitemaps": sitemaps}, {"sitemaps": sitemaps},
name="sitemap-detail", name="sitemap-detail",
), ),
path("download/<str:order_product_uuid>/", download_digital_asset_view, name="download_digital_asset"), path("download/<str:order_product_uuid>/", DownloadDigitalAssetView.as_view(), name="download_digital_asset"),
path("search/", GlobalSearchView.as_view(), name="global_search"), path("search/", GlobalSearchView.as_view(), name="global_search"),
path("app/cache/", CacheOperatorView.as_view(), name="cache_operator"), path("app/cache/", CacheOperatorView.as_view(), name="cache_operator"),
path("app/languages/", SupportedLanguagesView.as_view(), name="supported_languages"), path("app/languages/", SupportedLanguagesView.as_view(), name="supported_languages"),

View file

@ -14,6 +14,8 @@ from core.serializers import (
AttributeSimpleSerializer, AttributeSimpleSerializer,
AttributeValueDetailSerializer, AttributeValueDetailSerializer,
AttributeValueSimpleSerializer, AttributeValueSimpleSerializer,
BrandDetailSerializer,
BrandSimpleSerializer,
BulkAddOrderProductsSerializer, BulkAddOrderProductsSerializer,
BulkAddWishlistProductSerializer, BulkAddWishlistProductSerializer,
BulkRemoveOrderProductsSerializer, BulkRemoveOrderProductsSerializer,
@ -29,9 +31,21 @@ from core.serializers import (
OrderProductSimpleSerializer, OrderProductSimpleSerializer,
OrderSimpleSerializer, OrderSimpleSerializer,
ProductDetailSerializer, ProductDetailSerializer,
ProductImageDetailSerializer,
ProductImageSimpleSerializer,
ProductSimpleSerializer, ProductSimpleSerializer,
ProductTagDetailSerializer,
ProductTagSimpleSerializer,
PromoCodeDetailSerializer,
PromoCodeSimpleSerializer,
PromotionDetailSerializer,
PromotionSimpleSerializer,
RemoveOrderProductSerializer, RemoveOrderProductSerializer,
RemoveWishlistProductSerializer, RemoveWishlistProductSerializer,
StockDetailSerializer,
StockSimpleSerializer,
VendorDetailSerializer,
VendorSimpleSerializer,
WishlistDetailSerializer, WishlistDetailSerializer,
WishlistSimpleSerializer, WishlistSimpleSerializer,
) )
@ -652,3 +666,196 @@ ORDER_PRODUCT_SCHEMA = {
}, },
), ),
} }
BRAND_SCHEMA = {
"list": extend_schema(
summary=_("list all brands (simple view)"),
responses={status.HTTP_200_OK: BrandSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single brand (detailed view)"),
responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a brand"),
responses={status.HTTP_201_CREATED: BrandDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a brand"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing brand saving non-editables"),
responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing brand saving non-editables"),
responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS},
),
"seo_meta": extend_schema(
summary=_("SEO Meta snapshot for brand"),
responses={status.HTTP_200_OK: SeoSnapshotSerializer(), **BASE_ERRORS},
),
}
VENDOR_SCHEMA = {
"list": extend_schema(
summary=_("list all vendors (simple view)"),
responses={status.HTTP_200_OK: VendorSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single vendor (detailed view)"),
responses={status.HTTP_200_OK: VendorDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a vendor"),
responses={status.HTTP_201_CREATED: VendorDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a vendor"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing vendor saving non-editables"),
responses={status.HTTP_200_OK: VendorDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing vendor saving non-editables"),
responses={status.HTTP_200_OK: VendorDetailSerializer(), **BASE_ERRORS},
),
}
PRODUCT_IMAGE_SCHEMA = {
"list": extend_schema(
summary=_("list all product images (simple view)"),
responses={status.HTTP_200_OK: ProductImageSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single product image (detailed view)"),
responses={status.HTTP_200_OK: ProductImageDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a product image"),
responses={status.HTTP_201_CREATED: ProductImageDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a product image"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing product image saving non-editables"),
responses={status.HTTP_200_OK: ProductImageDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing product image saving non-editables"),
responses={status.HTTP_200_OK: ProductImageDetailSerializer(), **BASE_ERRORS},
),
}
PROMOCODE_SCHEMA = {
"list": extend_schema(
summary=_("list all promo codes (simple view)"),
responses={status.HTTP_200_OK: PromoCodeSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single promo code (detailed view)"),
responses={status.HTTP_200_OK: PromoCodeDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a promo code"),
responses={status.HTTP_201_CREATED: PromoCodeDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a promo code"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing promo code saving non-editables"),
responses={status.HTTP_200_OK: PromoCodeDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing promo code saving non-editables"),
responses={status.HTTP_200_OK: PromoCodeDetailSerializer(), **BASE_ERRORS},
),
}
PROMOTION_SCHEMA = {
"list": extend_schema(
summary=_("list all promotions (simple view)"),
responses={status.HTTP_200_OK: PromotionSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single promotion (detailed view)"),
responses={status.HTTP_200_OK: PromotionDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a promotion"),
responses={status.HTTP_201_CREATED: PromotionDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a promotion"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing promotion saving non-editables"),
responses={status.HTTP_200_OK: PromotionDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing promotion saving non-editables"),
responses={status.HTTP_200_OK: PromotionDetailSerializer(), **BASE_ERRORS},
),
}
STOCK_SCHEMA = {
"list": extend_schema(
summary=_("list all stocks (simple view)"),
responses={status.HTTP_200_OK: StockSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single stock (detailed view)"),
responses={status.HTTP_200_OK: StockDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a stock record"),
responses={status.HTTP_201_CREATED: StockDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a stock record"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing stock record saving non-editables"),
responses={status.HTTP_200_OK: StockDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing stock record saving non-editables"),
responses={status.HTTP_200_OK: StockDetailSerializer(), **BASE_ERRORS},
),
}
PRODUCT_TAG_SCHEMA = {
"list": extend_schema(
summary=_("list all product tags (simple view)"),
responses={status.HTTP_200_OK: ProductTagSimpleSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single product tag (detailed view)"),
responses={status.HTTP_200_OK: ProductTagDetailSerializer(), **BASE_ERRORS},
),
"create": extend_schema(
summary=_("create a product tag"),
responses={status.HTTP_201_CREATED: ProductTagDetailSerializer(), **BASE_ERRORS},
),
"destroy": extend_schema(
summary=_("delete a product tag"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
),
"update": extend_schema(
summary=_("rewrite an existing product tag saving non-editables"),
responses={status.HTTP_200_OK: ProductTagDetailSerializer(), **BASE_ERRORS},
),
"partial_update": extend_schema(
summary=_("rewrite some fields of an existing product tag saving non-editables"),
responses={status.HTTP_200_OK: ProductTagDetailSerializer(), **BASE_ERRORS},
),
}

View file

@ -301,66 +301,64 @@ class BuyAsBusinessView(APIView):
) )
def download_digital_asset_view(request: HttpRequest, *args, **kwargs) -> FileResponse | JsonResponse: class DownloadDigitalAssetView(APIView):
try: __doc__ = _( # type: ignore [assignment]
logger.debug(f"download_digital_asset_view: {kwargs}") "Handles the downloading of a digital asset associated with an order.\n"
op_uuid = str(kwargs.get("order_product_uuid")) "This function attempts to serve the digital asset file located in the "
if not op_uuid: "storage directory of the project. If the file is not found, an HTTP 404 "
raise BadRequest(_("order_product_uuid is required")) "error is raised to indicate the resource is unavailable."
uuid = urlsafe_base64_decode(op_uuid).decode("utf-8") )
download = DigitalAssetDownload.objects.get(order_product__uuid=uuid) def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse | FileResponse:
try:
op_uuid = str(kwargs.get("order_product_uuid"))
if not op_uuid:
raise BadRequest(_("order_product_uuid is required"))
uuid = urlsafe_base64_decode(op_uuid).decode("utf-8")
if download.num_downloads >= 1: download = DigitalAssetDownload.objects.get(order_product__uuid=uuid)
raise BadRequest(_("you can only download the digital asset once"))
if download.order_product.status != "FINISHED": if download.num_downloads >= 1:
raise BadRequest(_("the order must be paid before downloading the digital asset")) raise BadRequest(_("you can only download the digital asset once"))
download.num_downloads += 1 if download.order_product.status != "FINISHED":
download.save() raise BadRequest(_("the order must be paid before downloading the digital asset"))
if not download.order_product.product: download.num_downloads += 1
raise BadRequest(_("the order product does not have a product")) download.save()
file_path = download.order_product.product.stocks.first().digital_asset.path # type: ignore [union-attr] if not download.order_product.product:
raise BadRequest(_("the order product does not have a product"))
content_type, encoding = mimetypes.guess_type(file_path) file_path = download.order_product.product.stocks.first().digital_asset.path # type: ignore [union-attr]
if not content_type:
content_type = "application/octet-stream"
response = FileResponse(open(file_path, "rb"), content_type=content_type) content_type, encoding = mimetypes.guess_type(file_path)
filename = os.path.basename(file_path) if not content_type:
response["Content-Disposition"] = f'attachment; filename="{filename}"' content_type = "application/octet-stream"
return response
except BadRequest as e: response = FileResponse(open(file_path, "rb"), content_type=content_type)
return JsonResponse(camelize({"error": str(e)}), status=400) filename = os.path.basename(file_path)
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
except DigitalAssetDownload.DoesNotExist: except BadRequest as e:
return JsonResponse(camelize({"error": "Digital asset not found"}), status=404) return Response(data=camelize({"error": str(e)}), status=status.HTTP_400_BAD_REQUEST)
except Exception as e: except DigitalAssetDownload.DoesNotExist:
capture_exception(e) return Response(data=camelize({"error": "Digital asset not found"}), status=status.HTTP_404_NOT_FOUND)
return JsonResponse(
camelize(
{
"error": "An error occurred while trying to download the digital asset",
"traceback": traceback.format_exc() if settings.DEBUG else None,
"received": {"order_product_uuid": kwargs.get("order_product_uuid", "")},
}
),
status=500,
)
except Exception as e:
# noinspection PyTypeChecker capture_exception(e)
download_digital_asset_view.__doc__ = _( # type: ignore [assignment] return Response(
"Handles the downloading of a digital asset associated with an order.\n" data=camelize(
"This function attempts to serve the digital asset file located in the " {
"storage directory of the project. If the file is not found, an HTTP 404 " "error": "An error occurred while trying to download the digital asset",
"error is raised to indicate the resource is unavailable." "traceback": traceback.format_exc() if settings.DEBUG else None,
) "received": {"order_product_uuid": kwargs.get("order_product_uuid", "")},
}
),
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
def favicon_view(request: HttpRequest, *args, **kwargs) -> HttpResponse | FileResponse: def favicon_view(request: HttpRequest, *args, **kwargs) -> HttpResponse | FileResponse:

View file

@ -31,11 +31,18 @@ from core.docs.drf.viewsets import (
ATTRIBUTE_GROUP_SCHEMA, ATTRIBUTE_GROUP_SCHEMA,
ATTRIBUTE_SCHEMA, ATTRIBUTE_SCHEMA,
ATTRIBUTE_VALUE_SCHEMA, ATTRIBUTE_VALUE_SCHEMA,
BRAND_SCHEMA,
CATEGORY_SCHEMA, CATEGORY_SCHEMA,
FEEDBACK_SCHEMA, FEEDBACK_SCHEMA,
ORDER_PRODUCT_SCHEMA, ORDER_PRODUCT_SCHEMA,
ORDER_SCHEMA, ORDER_SCHEMA,
PRODUCT_IMAGE_SCHEMA,
PRODUCT_SCHEMA, PRODUCT_SCHEMA,
PRODUCT_TAG_SCHEMA,
PROMOCODE_SCHEMA,
PROMOTION_SCHEMA,
STOCK_SCHEMA,
VENDOR_SCHEMA,
WISHLIST_SCHEMA, WISHLIST_SCHEMA,
) )
from core.filters import AddressFilter, BrandFilter, CategoryFilter, FeedbackFilter, OrderFilter, ProductFilter from core.filters import AddressFilter, BrandFilter, CategoryFilter, FeedbackFilter, OrderFilter, ProductFilter
@ -313,6 +320,7 @@ class CategoryViewSet(EvibesViewSet):
return Response(SeoSnapshotSerializer(payload).data) return Response(SeoSnapshotSerializer(payload).data)
@extend_schema_view(**BRAND_SCHEMA)
class BrandViewSet(EvibesViewSet): class BrandViewSet(EvibesViewSet):
__doc__ = _( __doc__ = _(
"Represents a viewset for managing Brand instances. " "Represents a viewset for managing Brand instances. "
@ -554,6 +562,7 @@ class ProductViewSet(EvibesViewSet):
return Response(SeoSnapshotSerializer(payload).data) return Response(SeoSnapshotSerializer(payload).data)
@extend_schema_view(**VENDOR_SCHEMA)
class VendorViewSet(EvibesViewSet): class VendorViewSet(EvibesViewSet):
__doc__ = _( __doc__ = _(
"Represents a viewset for managing Vendor objects. " "Represents a viewset for managing Vendor objects. "
@ -853,6 +862,7 @@ class OrderProductViewSet(EvibesViewSet):
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
@extend_schema_view(**PRODUCT_IMAGE_SCHEMA)
class ProductImageViewSet(EvibesViewSet): class ProductImageViewSet(EvibesViewSet):
__doc__ = _("Manages operations related to Product images in the application. ") __doc__ = _("Manages operations related to Product images in the application. ")
@ -865,6 +875,7 @@ class ProductImageViewSet(EvibesViewSet):
} }
@extend_schema_view(**PROMOCODE_SCHEMA)
class PromoCodeViewSet(EvibesViewSet): class PromoCodeViewSet(EvibesViewSet):
__doc__ = _("Manages the retrieval and handling of PromoCode instances through various API actions.") __doc__ = _("Manages the retrieval and handling of PromoCode instances through various API actions.")
@ -886,6 +897,7 @@ class PromoCodeViewSet(EvibesViewSet):
return qs.filter(user=user) return qs.filter(user=user)
@extend_schema_view(**PROMOTION_SCHEMA)
class PromotionViewSet(EvibesViewSet): class PromotionViewSet(EvibesViewSet):
__doc__ = _("Represents a view set for managing promotions. ") __doc__ = _("Represents a view set for managing promotions. ")
@ -898,6 +910,7 @@ class PromotionViewSet(EvibesViewSet):
} }
@extend_schema_view(**STOCK_SCHEMA)
class StockViewSet(EvibesViewSet): class StockViewSet(EvibesViewSet):
__doc__ = _("Handles operations related to Stock data in the system.") __doc__ = _("Handles operations related to Stock data in the system.")
@ -1100,6 +1113,7 @@ class AddressViewSet(EvibesViewSet):
) )
@extend_schema_view(**PRODUCT_TAG_SCHEMA)
class ProductTagViewSet(EvibesViewSet): class ProductTagViewSet(EvibesViewSet):
__doc__ = _( __doc__ = _(
"Handles operations related to Product Tags within the application. " "Handles operations related to Product Tags within the application. "

View file

@ -0,0 +1,17 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework import status
from core.docs.drf import BASE_ERRORS
from payments.serializers import TransactionSerializer
TRANSACTION_SCHEMA = {
"list": extend_schema(
summary=_("list all transactions (read-only)"),
responses={status.HTTP_200_OK: TransactionSerializer(many=True), **BASE_ERRORS},
),
"retrieve": extend_schema(
summary=_("retrieve a single transaction (read-only)"),
responses={status.HTTP_200_OK: TransactionSerializer(), **BASE_ERRORS},
),
}

View file

@ -1,10 +1,13 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema_view
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from core.permissions import EvibesPermission, IsOwner from core.permissions import EvibesPermission, IsOwner
from payments.serializers import TransactionSerializer from payments.serializers import TransactionSerializer
from payments.docs.drf.viewsets import TRANSACTION_SCHEMA
@extend_schema_view(**TRANSACTION_SCHEMA)
class TransactionViewSet(ReadOnlyModelViewSet): # type: ignore class TransactionViewSet(ReadOnlyModelViewSet): # type: ignore
__doc__ = _( # type: ignore [assignment] __doc__ = _( # type: ignore [assignment]
"ViewSet for handling read-only operations on the Transaction model. " "ViewSet for handling read-only operations on the Transaction model. "