Same gap as the stores endpoint: store/category/search filters on GET /api/v1/products/ already worked but weren't declared to drf-spectacular. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
from django.db.models import Q
|
|
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
|
|
from rest_framework import mixins, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
|
|
|
from apps.core.permissions import IsStoreOwner
|
|
from apps.stores.models import Store
|
|
|
|
from .models import Product, ProductCategory
|
|
from .serializers import (
|
|
ProductCategorySerializer,
|
|
ProductDetailSerializer,
|
|
ProductListSerializer,
|
|
SellerProductSerializer,
|
|
StockAdjustSerializer,
|
|
)
|
|
|
|
|
|
class ProductCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
|
schema_tags = ['Catalog']
|
|
permission_classes = [AllowAny]
|
|
serializer_class = ProductCategorySerializer
|
|
queryset = ProductCategory.objects.all()
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(
|
|
parameters=[
|
|
OpenApiParameter('store', str, description='Filter by store UUID.'),
|
|
OpenApiParameter('category', str, description='Filter by product category UUID.'),
|
|
OpenApiParameter('search', str, description='Search by product name/description.'),
|
|
],
|
|
),
|
|
)
|
|
class ProductViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
"""Customer-facing product browsing (store page, product detail, search)."""
|
|
|
|
schema_tags = ['Catalog']
|
|
permission_classes = [AllowAny]
|
|
queryset = Product.objects.filter(
|
|
is_active=True, store__status=Store.Status.APPROVED,
|
|
).select_related('store', 'category')
|
|
|
|
def get_serializer_class(self):
|
|
if self.action == 'retrieve':
|
|
return ProductDetailSerializer
|
|
return ProductListSerializer
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset()
|
|
|
|
store_uuid = self.request.query_params.get('store')
|
|
if store_uuid:
|
|
queryset = queryset.filter(store__uuid=store_uuid)
|
|
|
|
category_uuid = self.request.query_params.get('category')
|
|
if category_uuid:
|
|
queryset = queryset.filter(category__uuid=category_uuid)
|
|
|
|
search = self.request.query_params.get('search')
|
|
if search:
|
|
queryset = queryset.filter(Q(name__icontains=search) | Q(description__icontains=search))
|
|
|
|
return queryset
|
|
|
|
|
|
class SellerProductViewSet(viewsets.ModelViewSet):
|
|
"""Seller's own product management (S06 list, S07 add, S08 inventory)."""
|
|
|
|
schema_tags = ['Seller · Products']
|
|
serializer_class = SellerProductSerializer
|
|
# permission_classes = [IsAuthenticated, IsStoreOwner]
|
|
# TODO: IsStoreOwner?
|
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
|
required_alternate_scopes = {
|
|
"POST": [[]],
|
|
}
|
|
|
|
def get_queryset(self):
|
|
if getattr(self, 'swagger_fake_view', False):
|
|
return Product.objects.none()
|
|
return Product.objects.filter(store=self.request.user.store).select_related('category')
|
|
|
|
@action(detail=True, methods=['patch'], url_path='stock')
|
|
def adjust_stock(self, request, pk=None):
|
|
product = self.get_object()
|
|
serializer = StockAdjustSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
product.stock_quantity = serializer.validated_data['stock_quantity']
|
|
product.save(update_fields=['stock_quantity', 'updated_at'])
|
|
return Response(SellerProductSerializer(product).data)
|