Compare commits
No commits in common. "6796922fe68f9856f1c2e4938c1aa51dceb9e915" and "2b3fd0fc28e34f13321ed11ad7a2db7bcaf30676" have entirely different histories.
6796922fe6
...
2b3fd0fc28
13 changed files with 68 additions and 171 deletions
|
|
@ -7,12 +7,5 @@
|
||||||
- `GET /api/v1/products/` — same issue: documented the existing `store`, `category`, and `search` query params for swagger (`apps/catalog/views.py`).
|
- `GET /api/v1/products/` — same issue: documented the existing `store`, `category`, and `search` query params for swagger (`apps/catalog/views.py`).
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `GET /api/v1/cities/?search=` — search cities by name (`apps/locations/views.py`); previously no text search existed.
|
|
||||||
- `GET /api/v1/neighborhoods/?search=` — search neighborhoods by neighborhood name or city name, alongside the existing `city` UUID filter (`apps/locations/views.py`).
|
|
||||||
- `GET /api/v1/stores/` now returns `phone_number` in the list (previously only on the single-store detail view) and a `distance_km` field, populated whenever `lat`/`lng` are passed (`apps/stores/serializers.py`).
|
|
||||||
- Test coverage for `StoreViewSet` list filtering (`apps/stores/tests/test_store_list.py`) — previously untested.
|
- Test coverage for `StoreViewSet` list filtering (`apps/stores/tests/test_store_list.py`) — previously untested.
|
||||||
- Test coverage for `ProductViewSet` list filtering (`apps/catalog/tests/test_product_list.py`) — previously untested.
|
- Test coverage for `ProductViewSet` list filtering (`apps/catalog/tests/test_product_list.py`) — previously untested.
|
||||||
- Test coverage for `CityViewSet`/`NeighborhoodViewSet` list filtering (`apps/locations/tests/test_location_list.py`) — previously untested.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Replaced hand-rolled `get_queryset` filtering with `django-filter` `FilterSet` classes (`apps/{stores,catalog,locations,reviews}/filters.py`) and `filterset_fields` (`apps/orders` status). `django-filter` was already installed and set as `DEFAULT_FILTER_BACKENDS` but unused everywhere; params are now auto-documented in swagger by drf-spectacular's django-filter integration, so the manual `OpenApiParameter` declarations for those fields were removed (stores keeps `lat`/`lng` manual since geo-distance isn't a plain filter).
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
from django.db.models import Q
|
|
||||||
from django_filters import rest_framework as filters
|
|
||||||
|
|
||||||
from .models import Product
|
|
||||||
|
|
||||||
|
|
||||||
class ProductFilter(filters.FilterSet):
|
|
||||||
store = filters.UUIDFilter(field_name='store__uuid', help_text='Filter by store UUID.')
|
|
||||||
category = filters.UUIDFilter(field_name='category__uuid', help_text='Filter by product category UUID.')
|
|
||||||
search = filters.CharFilter(method='filter_search', help_text='Search by product name/description.')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Product
|
|
||||||
fields = ['store', 'category', 'search']
|
|
||||||
|
|
||||||
def filter_search(self, queryset, name, value):
|
|
||||||
return queryset.filter(Q(name__icontains=value) | Q(description__icontains=value))
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
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 import mixins, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
|
@ -8,7 +10,6 @@ from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRe
|
||||||
from apps.core.permissions import IsStoreOwner
|
from apps.core.permissions import IsStoreOwner
|
||||||
from apps.stores.models import Store
|
from apps.stores.models import Store
|
||||||
|
|
||||||
from .filters import ProductFilter
|
|
||||||
from .models import Product, ProductCategory
|
from .models import Product, ProductCategory
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
ProductCategorySerializer,
|
ProductCategorySerializer,
|
||||||
|
|
@ -26,6 +27,15 @@ class ProductCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||||
queryset = ProductCategory.objects.all()
|
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):
|
class ProductViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
"""Customer-facing product browsing (store page, product detail, search)."""
|
"""Customer-facing product browsing (store page, product detail, search)."""
|
||||||
|
|
||||||
|
|
@ -34,13 +44,29 @@ class ProductViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.
|
||||||
queryset = Product.objects.filter(
|
queryset = Product.objects.filter(
|
||||||
is_active=True, store__status=Store.Status.APPROVED,
|
is_active=True, store__status=Store.Status.APPROVED,
|
||||||
).select_related('store', 'category')
|
).select_related('store', 'category')
|
||||||
filterset_class = ProductFilter
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == 'retrieve':
|
if self.action == 'retrieve':
|
||||||
return ProductDetailSerializer
|
return ProductDetailSerializer
|
||||||
return ProductListSerializer
|
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):
|
class SellerProductViewSet(viewsets.ModelViewSet):
|
||||||
"""Seller's own product management (S06 list, S07 add, S08 inventory)."""
|
"""Seller's own product management (S06 list, S07 add, S08 inventory)."""
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
from django.db.models import Q
|
|
||||||
from django_filters import rest_framework as filters
|
|
||||||
|
|
||||||
from .models import City, Neighborhood
|
|
||||||
|
|
||||||
|
|
||||||
class CityFilter(filters.FilterSet):
|
|
||||||
search = filters.CharFilter(field_name='name', lookup_expr='icontains', help_text='Search by city name.')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = City
|
|
||||||
fields = ['search']
|
|
||||||
|
|
||||||
|
|
||||||
class NeighborhoodFilter(filters.FilterSet):
|
|
||||||
city = filters.UUIDFilter(field_name='city__uuid', help_text='Filter by city UUID.')
|
|
||||||
search = filters.CharFilter(method='filter_search', help_text='Search by neighborhood or city name.')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Neighborhood
|
|
||||||
fields = ['city', 'search']
|
|
||||||
|
|
||||||
def filter_search(self, queryset, name, value):
|
|
||||||
return queryset.filter(Q(name__icontains=value) | Q(city__name__icontains=value))
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from apps.locations.models import City, Neighborhood
|
|
||||||
|
|
||||||
|
|
||||||
class LocationListFilterTests(APITestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.tehran = City.objects.create(name='تهران', slug='tehran')
|
|
||||||
cls.mashhad = City.objects.create(name='مشهد', slug='mashhad')
|
|
||||||
|
|
||||||
cls.vanak = Neighborhood.objects.create(city=cls.tehran, name='ونک', slug='vanak')
|
|
||||||
cls.tajrish = Neighborhood.objects.create(city=cls.tehran, name='تجریش', slug='tajrish')
|
|
||||||
cls.mashhad_hood = Neighborhood.objects.create(city=cls.mashhad, name='احمدآباد', slug='ahmadabad')
|
|
||||||
|
|
||||||
def test_search_city_by_name(self):
|
|
||||||
response = self.client.get('/api/v1/cities/', {'search': 'مشهد'})
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
uuids = {city['uuid'] for city in response.data['results']}
|
|
||||||
self.assertEqual(uuids, {str(self.mashhad.uuid)})
|
|
||||||
|
|
||||||
def test_filter_neighborhood_by_city(self):
|
|
||||||
response = self.client.get('/api/v1/neighborhoods/', {'city': str(self.tehran.uuid)})
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
uuids = {n['uuid'] for n in response.data['results']}
|
|
||||||
self.assertEqual(uuids, {str(self.vanak.uuid), str(self.tajrish.uuid)})
|
|
||||||
|
|
||||||
def test_search_neighborhood_by_name(self):
|
|
||||||
response = self.client.get('/api/v1/neighborhoods/', {'search': 'ونک'})
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
uuids = {n['uuid'] for n in response.data['results']}
|
|
||||||
self.assertEqual(uuids, {str(self.vanak.uuid)})
|
|
||||||
|
|
||||||
def test_search_neighborhood_by_city_name(self):
|
|
||||||
response = self.client.get('/api/v1/neighborhoods/', {'search': 'مشهد'})
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
uuids = {n['uuid'] for n in response.data['results']}
|
|
||||||
self.assertEqual(uuids, {str(self.mashhad_hood.uuid)})
|
|
||||||
|
|
@ -3,7 +3,6 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
|
||||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
|
|
||||||
from .filters import CityFilter, NeighborhoodFilter
|
|
||||||
from .models import Address, City, Neighborhood
|
from .models import Address, City, Neighborhood
|
||||||
from .serializers import AddressSerializer, CitySerializer, NeighborhoodSerializer
|
from .serializers import AddressSerializer, CitySerializer, NeighborhoodSerializer
|
||||||
|
|
||||||
|
|
@ -13,7 +12,6 @@ class CityViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Gen
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
serializer_class = CitySerializer
|
serializer_class = CitySerializer
|
||||||
queryset = City.objects.filter(is_active=True)
|
queryset = City.objects.filter(is_active=True)
|
||||||
filterset_class = CityFilter
|
|
||||||
|
|
||||||
|
|
||||||
class NeighborhoodViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
class NeighborhoodViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
|
|
@ -21,7 +19,13 @@ class NeighborhoodViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, view
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
serializer_class = NeighborhoodSerializer
|
serializer_class = NeighborhoodSerializer
|
||||||
queryset = Neighborhood.objects.filter(is_active=True).select_related('city')
|
queryset = Neighborhood.objects.filter(is_active=True).select_related('city')
|
||||||
filterset_class = NeighborhoodFilter
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
city_uuid = self.request.query_params.get('city')
|
||||||
|
if city_uuid:
|
||||||
|
queryset = queryset.filter(city__uuid=city_uuid)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class AddressViewSet(viewsets.ModelViewSet):
|
class AddressViewSet(viewsets.ModelViewSet):
|
||||||
|
|
|
||||||
|
|
@ -160,12 +160,15 @@ class SellerOrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, views
|
||||||
required_alternate_scopes = {
|
required_alternate_scopes = {
|
||||||
"POST": [[]],
|
"POST": [[]],
|
||||||
}
|
}
|
||||||
filterset_fields = ['status']
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if getattr(self, 'swagger_fake_view', False):
|
if getattr(self, 'swagger_fake_view', False):
|
||||||
return Order.objects.none()
|
return Order.objects.none()
|
||||||
return Order.objects.filter(store=self.request.user.store).select_related('store').prefetch_related('items')
|
queryset = Order.objects.filter(store=self.request.user.store).select_related('store').prefetch_related('items')
|
||||||
|
status_param = self.request.query_params.get('status')
|
||||||
|
if status_param:
|
||||||
|
queryset = queryset.filter(status=status_param)
|
||||||
|
return queryset
|
||||||
|
|
||||||
def _apply_transition(self, request, to_status):
|
def _apply_transition(self, request, to_status):
|
||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
from django_filters import rest_framework as filters
|
|
||||||
|
|
||||||
from .models import Review
|
|
||||||
|
|
||||||
|
|
||||||
class ReviewFilter(filters.FilterSet):
|
|
||||||
store = filters.UUIDFilter(field_name='store__uuid', help_text='Filter by store UUID.')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Review
|
|
||||||
fields = ['store']
|
|
||||||
|
|
@ -6,7 +6,6 @@ from rest_framework.response import Response
|
||||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
from apps.core.permissions import IsStoreOwner
|
from apps.core.permissions import IsStoreOwner
|
||||||
|
|
||||||
from .filters import ReviewFilter
|
|
||||||
from .models import Review
|
from .models import Review
|
||||||
from .serializers import ReviewSerializer, SellerReplySerializer
|
from .serializers import ReviewSerializer, SellerReplySerializer
|
||||||
|
|
||||||
|
|
@ -20,14 +19,19 @@ class ReviewViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.Gen
|
||||||
required_alternate_scopes = {
|
required_alternate_scopes = {
|
||||||
"POST": [[]],
|
"POST": [[]],
|
||||||
}
|
}
|
||||||
filterset_class = ReviewFilter
|
|
||||||
queryset = Review.objects.select_related('customer', 'store')
|
|
||||||
|
|
||||||
def get_permissions(self):
|
def get_permissions(self):
|
||||||
if self.action == 'create':
|
if self.action == 'create':
|
||||||
return [IsAuthenticatedOrTokenMatchesOASRequirements]
|
return [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
return [AllowAny()]
|
return [AllowAny()]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = Review.objects.select_related('customer', 'store')
|
||||||
|
store_uuid = self.request.query_params.get('store')
|
||||||
|
if store_uuid:
|
||||||
|
queryset = queryset.filter(store__uuid=store_uuid)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class SellerReviewViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
class SellerReviewViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||||
"""Reviews left for the authenticated seller's store, with reply support."""
|
"""Reviews left for the authenticated seller's store, with reply support."""
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
from django.db.models import Q
|
|
||||||
from django_filters import rest_framework as filters
|
|
||||||
|
|
||||||
from .models import Store
|
|
||||||
|
|
||||||
|
|
||||||
class StoreFilter(filters.FilterSet):
|
|
||||||
category = filters.UUIDFilter(field_name='category__uuid', help_text='Filter by store category UUID.')
|
|
||||||
neighborhood = filters.UUIDFilter(
|
|
||||||
field_name='service_neighborhoods__uuid', help_text='Filter by service neighborhood UUID.',
|
|
||||||
)
|
|
||||||
search = filters.CharFilter(method='filter_search', help_text='Search by store name/description.')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Store
|
|
||||||
fields = ['category', 'neighborhood', 'search']
|
|
||||||
|
|
||||||
def filter_search(self, queryset, name, value):
|
|
||||||
return queryset.filter(Q(name__icontains=value) | Q(description__icontains=value))
|
|
||||||
|
|
@ -31,14 +31,13 @@ class StoreListSerializer(serializers.ModelSerializer):
|
||||||
category = StoreCategorySerializer(read_only=True)
|
category = StoreCategorySerializer(read_only=True)
|
||||||
logo_url = serializers.SerializerMethodField()
|
logo_url = serializers.SerializerMethodField()
|
||||||
cover_image_url = serializers.SerializerMethodField()
|
cover_image_url = serializers.SerializerMethodField()
|
||||||
distance_km = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Store
|
model = Store
|
||||||
fields = (
|
fields = (
|
||||||
'uuid', 'name', 'category', 'logo', 'logo_url', 'cover_image', 'cover_image_url',
|
'uuid', 'name', 'category', 'logo', 'logo_url', 'cover_image', 'cover_image_url',
|
||||||
'phone_number', 'rating_avg', 'rating_count', 'min_order_amount', 'delivery_fee',
|
'rating_avg', 'rating_count', 'min_order_amount', 'delivery_fee',
|
||||||
'free_delivery_threshold', 'is_open', 'distance_km',
|
'free_delivery_threshold', 'is_open',
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema_field(serializers.URLField(allow_null=True))
|
@extend_schema_field(serializers.URLField(allow_null=True))
|
||||||
|
|
@ -49,19 +48,13 @@ class StoreListSerializer(serializers.ModelSerializer):
|
||||||
def get_cover_image_url(self, obj):
|
def get_cover_image_url(self, obj):
|
||||||
return presigned_media_url(obj.cover_image)
|
return presigned_media_url(obj.cover_image)
|
||||||
|
|
||||||
@extend_schema_field(serializers.FloatField(allow_null=True))
|
|
||||||
def get_distance_km(self, obj):
|
|
||||||
"""Only present when the request passed `lat`/`lng` (see StoreViewSet.get_queryset)."""
|
|
||||||
distance = getattr(obj, 'distance', None)
|
|
||||||
return round(distance.km, 2) if distance is not None else None
|
|
||||||
|
|
||||||
|
|
||||||
class StoreDetailSerializer(StoreListSerializer):
|
class StoreDetailSerializer(StoreListSerializer):
|
||||||
working_hours = StoreWorkingHoursSerializer(many=True, read_only=True)
|
working_hours = StoreWorkingHoursSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta(StoreListSerializer.Meta):
|
class Meta(StoreListSerializer.Meta):
|
||||||
fields = StoreListSerializer.Meta.fields + (
|
fields = StoreListSerializer.Meta.fields + (
|
||||||
'description', 'address',
|
'description', 'address', 'phone_number',
|
||||||
'accepts_wallet', 'accepts_online', 'accepts_cash_on_delivery', 'working_hours',
|
'accepts_wallet', 'accepts_online', 'accepts_cash_on_delivery', 'working_hours',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from django.contrib.gis.geos import Point
|
|
||||||
|
|
||||||
from apps.stores.models import StoreCategory
|
from apps.stores.models import StoreCategory
|
||||||
|
|
||||||
from apps.orders.tests.base import OrdersTestCase
|
from apps.orders.tests.base import OrdersTestCase
|
||||||
|
|
@ -23,28 +21,3 @@ class StoreListFilterTests(OrdersTestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
uuids = {store['uuid'] for store in response.data['results']}
|
uuids = {store['uuid'] for store in response.data['results']}
|
||||||
self.assertEqual(uuids, {str(self.store1.uuid), str(self.store2.uuid)})
|
self.assertEqual(uuids, {str(self.store1.uuid), str(self.store2.uuid)})
|
||||||
|
|
||||||
def test_phone_number_included_in_list(self):
|
|
||||||
self.store1.phone_number = '02112345678'
|
|
||||||
self.store1.save(update_fields=['phone_number'])
|
|
||||||
|
|
||||||
response = self.client.get('/api/v1/stores/')
|
|
||||||
|
|
||||||
store = next(s for s in response.data['results'] if s['uuid'] == str(self.store1.uuid))
|
|
||||||
self.assertEqual(store['phone_number'], '02112345678')
|
|
||||||
|
|
||||||
def test_distance_km_present_when_lat_lng_given(self):
|
|
||||||
self.store1.location = Point(51.4, 35.75, srid=4326)
|
|
||||||
self.store1.save(update_fields=['location'])
|
|
||||||
|
|
||||||
response = self.client.get('/api/v1/stores/', {'lat': '35.75', 'lng': '51.4'})
|
|
||||||
|
|
||||||
store = next(s for s in response.data['results'] if s['uuid'] == str(self.store1.uuid))
|
|
||||||
self.assertIsNotNone(store['distance_km'])
|
|
||||||
self.assertAlmostEqual(store['distance_km'], 0, delta=0.1)
|
|
||||||
|
|
||||||
def test_distance_km_absent_without_lat_lng(self):
|
|
||||||
response = self.client.get('/api/v1/stores/')
|
|
||||||
|
|
||||||
store = next(s for s in response.data['results'] if s['uuid'] == str(self.store1.uuid))
|
|
||||||
self.assertIsNone(store['distance_km'])
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.gis.db.models.functions import Distance
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
from django.contrib.gis.measure import D
|
from django.contrib.gis.measure import D
|
||||||
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
|
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
|
||||||
from rest_framework import mixins, status, viewsets
|
from rest_framework import mixins, status, viewsets
|
||||||
|
|
@ -12,7 +13,6 @@ from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRe
|
||||||
|
|
||||||
from apps.core.permissions import IsStoreOwner
|
from apps.core.permissions import IsStoreOwner
|
||||||
|
|
||||||
from .filters import StoreFilter
|
|
||||||
from .models import Store, StoreCategory, StoreWorkingHours
|
from .models import Store, StoreCategory, StoreWorkingHours
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
SellerStoreSerializer,
|
SellerStoreSerializer,
|
||||||
|
|
@ -33,6 +33,9 @@ class StoreCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
list=extend_schema(
|
list=extend_schema(
|
||||||
parameters=[
|
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('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).'),
|
OpenApiParameter('lng', float, description='Longitude; used with `lat` to sort by distance (15km radius).'),
|
||||||
],
|
],
|
||||||
|
|
@ -43,8 +46,7 @@ class StoreViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
|
||||||
|
|
||||||
schema_tags = ['Stores']
|
schema_tags = ['Stores']
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
queryset = Store.objects.filter(status=Store.Status.APPROVED).select_related('category', 'city').distinct()
|
queryset = Store.objects.filter(status=Store.Status.APPROVED).select_related('category', 'city')
|
||||||
filterset_class = StoreFilter
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == 'retrieve':
|
if self.action == 'retrieve':
|
||||||
|
|
@ -54,6 +56,18 @@ class StoreViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
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')
|
lat = self.request.query_params.get('lat')
|
||||||
lng = self.request.query_params.get('lng')
|
lng = self.request.query_params.get('lng')
|
||||||
if lat and lng:
|
if lat and lng:
|
||||||
|
|
@ -62,7 +76,7 @@ class StoreViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
|
||||||
distance=Distance('location', point)
|
distance=Distance('location', point)
|
||||||
).order_by('distance')
|
).order_by('distance')
|
||||||
|
|
||||||
return queryset
|
return queryset.distinct()
|
||||||
|
|
||||||
|
|
||||||
class SellerStoreView(APIView):
|
class SellerStoreView(APIView):
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue