winofy-backend/apps/stores/views.py
Ali Asadi 4073081c25 Document store list query params for swagger; add filter test coverage
Category/neighborhood/search/lat/lng filters on GET /api/v1/stores/
already worked but weren't declared to drf-spectacular, so they never
showed up in swagger.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 14:19:48 +03:30

144 lines
5.8 KiB
Python

from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
from django.db.models import Q
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
from rest_framework import mixins, status, viewsets
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.core.permissions import IsStoreOwner
from .models import Store, StoreCategory, StoreWorkingHours
from .serializers import (
SellerStoreSerializer,
StoreCategorySerializer,
StoreDetailSerializer,
StoreListSerializer,
StoreWorkingHoursSerializer,
)
class StoreCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
schema_tags = ['Stores']
permission_classes = [AllowAny]
serializer_class = StoreCategorySerializer
queryset = StoreCategory.objects.all()
@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter('category', str, description='Filter by store category UUID.'),
OpenApiParameter('neighborhood', str, description='Filter by service neighborhood UUID.'),
OpenApiParameter('search', str, description='Search by store name/description.'),
OpenApiParameter('lat', float, description='Latitude; used with `lng` to sort by distance (15km radius).'),
OpenApiParameter('lng', float, description='Longitude; used with `lat` to sort by distance (15km radius).'),
],
),
)
class StoreViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
"""Customer-facing store browsing (home feed, store page)."""
schema_tags = ['Stores']
permission_classes = [AllowAny]
queryset = Store.objects.filter(status=Store.Status.APPROVED).select_related('category', 'city')
def get_serializer_class(self):
if self.action == 'retrieve':
return StoreDetailSerializer
return StoreListSerializer
def get_queryset(self):
queryset = super().get_queryset()
neighborhood_uuid = self.request.query_params.get('neighborhood')
if neighborhood_uuid:
queryset = queryset.filter(service_neighborhoods__uuid=neighborhood_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))
lat = self.request.query_params.get('lat')
lng = self.request.query_params.get('lng')
if lat and lng:
point = Point(float(lng), float(lat), srid=4326)
queryset = queryset.filter(location__distance_lte=(point, D(km=15))).annotate(
distance=Distance('location', point)
).order_by('distance')
return queryset.distinct()
class SellerStoreView(APIView):
"""The authenticated seller's own store — GET/PATCH to manage it, POST to create it."""
schema_tags = ['Seller · Store']
serializer_class = SellerStoreSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def get(self, request):
store = get_object_or_404(Store, owner=request.user)
return Response(SellerStoreSerializer(store, context={'request': request}).data)
def post(self, request):
if hasattr(request.user, 'store'):
return Response(
{'detail': 'شما قبلاً یک فروشگاه ثبت کرده‌اید.'}, status=status.HTTP_409_CONFLICT,
)
serializer = SellerStoreSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
@extend_schema(request=SellerStoreSerializer)
def patch(self, request):
store = get_object_or_404(Store, owner=request.user)
serializer = SellerStoreSerializer(
store, data=request.data, partial=True, context={'request': request},
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
class SellerStoreWorkingHoursView(APIView):
"""Bulk get/set the authenticated seller's weekly working hours (S14)."""
schema_tags = ['Seller · Store']
serializer_class = StoreWorkingHoursSerializer
# permission_classes = [IsAuthenticated, IsStoreOwner]
# TODO:
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def get(self, request):
hours = StoreWorkingHours.objects.filter(store=request.user.store)
return Response(StoreWorkingHoursSerializer(hours, many=True).data)
@extend_schema(request=StoreWorkingHoursSerializer(many=True))
def put(self, request):
store = request.user.store
entries = request.data if isinstance(request.data, list) else request.data.get('working_hours', [])
serializer = StoreWorkingHoursSerializer(data=entries, many=True)
serializer.is_valid(raise_exception=True)
StoreWorkingHours.objects.filter(store=store).delete()
StoreWorkingHours.objects.bulk_create(
[StoreWorkingHours(store=store, **entry) for entry in serializer.validated_data]
)
return Response(StoreWorkingHoursSerializer(store.working_hours.all(), many=True).data)