Migrate manual query-param filtering to django-filter
django-filter was already installed and set as DEFAULT_FILTER_BACKENDS but unused everywhere. Replace hand-rolled get_queryset filtering with FilterSet classes (stores/catalog/locations/reviews) and filterset_fields (orders status). drf-spectacular auto-documents these for swagger, so the manual OpenApiParameter declarations for the migrated fields are removed (stores keeps lat/lng manual since geo-distance isn't a plain filter). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
755d74a9ff
commit
eb5eb38665
10 changed files with 88 additions and 95 deletions
|
|
@ -13,3 +13,6 @@
|
|||
- 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 `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).
|
||||
|
|
|
|||
17
apps/catalog/filters.py
Normal file
17
apps/catalog/filters.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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,5 +1,3 @@
|
|||
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
|
||||
|
|
@ -10,6 +8,7 @@ from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRe
|
|||
from apps.core.permissions import IsStoreOwner
|
||||
from apps.stores.models import Store
|
||||
|
||||
from .filters import ProductFilter
|
||||
from .models import Product, ProductCategory
|
||||
from .serializers import (
|
||||
ProductCategorySerializer,
|
||||
|
|
@ -27,15 +26,6 @@ class ProductCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
|||
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)."""
|
||||
|
||||
|
|
@ -44,29 +34,13 @@ class ProductViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.
|
|||
queryset = Product.objects.filter(
|
||||
is_active=True, store__status=Store.Status.APPROVED,
|
||||
).select_related('store', 'category')
|
||||
filterset_class = ProductFilter
|
||||
|
||||
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)."""
|
||||
|
|
|
|||
24
apps/locations/filters.py
Normal file
24
apps/locations/filters.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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,61 +1,27 @@
|
|||
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.permissions import AllowAny, IsAuthenticated
|
||||
|
||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||
|
||||
from .filters import CityFilter, NeighborhoodFilter
|
||||
from .models import Address, City, Neighborhood
|
||||
from .serializers import AddressSerializer, CitySerializer, NeighborhoodSerializer
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter('search', str, description='Search by city name.'),
|
||||
],
|
||||
),
|
||||
)
|
||||
class CityViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||
schema_tags = ['Locations']
|
||||
permission_classes = [AllowAny]
|
||||
serializer_class = CitySerializer
|
||||
queryset = City.objects.filter(is_active=True)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
search = self.request.query_params.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(name__icontains=search)
|
||||
return queryset
|
||||
filterset_class = CityFilter
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter('city', str, description='Filter by city UUID.'),
|
||||
OpenApiParameter('search', str, description='Search by neighborhood or city name.'),
|
||||
],
|
||||
),
|
||||
)
|
||||
class NeighborhoodViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||
schema_tags = ['Locations']
|
||||
permission_classes = [AllowAny]
|
||||
serializer_class = NeighborhoodSerializer
|
||||
queryset = Neighborhood.objects.filter(is_active=True).select_related('city')
|
||||
|
||||
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)
|
||||
|
||||
search = self.request.query_params.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(Q(name__icontains=search) | Q(city__name__icontains=search))
|
||||
|
||||
return queryset
|
||||
filterset_class = NeighborhoodFilter
|
||||
|
||||
|
||||
class AddressViewSet(viewsets.ModelViewSet):
|
||||
|
|
|
|||
|
|
@ -160,15 +160,12 @@ class SellerOrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, views
|
|||
required_alternate_scopes = {
|
||||
"POST": [[]],
|
||||
}
|
||||
filterset_fields = ['status']
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return Order.objects.none()
|
||||
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
|
||||
return Order.objects.filter(store=self.request.user.store).select_related('store').prefetch_related('items')
|
||||
|
||||
def _apply_transition(self, request, to_status):
|
||||
order = self.get_object()
|
||||
|
|
|
|||
11
apps/reviews/filters.py
Normal file
11
apps/reviews/filters.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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,6 +6,7 @@ from rest_framework.response import Response
|
|||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||
from apps.core.permissions import IsStoreOwner
|
||||
|
||||
from .filters import ReviewFilter
|
||||
from .models import Review
|
||||
from .serializers import ReviewSerializer, SellerReplySerializer
|
||||
|
||||
|
|
@ -19,19 +20,14 @@ class ReviewViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.Gen
|
|||
required_alternate_scopes = {
|
||||
"POST": [[]],
|
||||
}
|
||||
filterset_class = ReviewFilter
|
||||
queryset = Review.objects.select_related('customer', 'store')
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == 'create':
|
||||
return [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||
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):
|
||||
"""Reviews left for the authenticated seller's store, with reply support."""
|
||||
|
|
|
|||
19
apps/stores/filters.py
Normal file
19
apps/stores/filters.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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))
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
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
|
||||
|
|
@ -13,6 +12,7 @@ from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRe
|
|||
|
||||
from apps.core.permissions import IsStoreOwner
|
||||
|
||||
from .filters import StoreFilter
|
||||
from .models import Store, StoreCategory, StoreWorkingHours
|
||||
from .serializers import (
|
||||
SellerStoreSerializer,
|
||||
|
|
@ -33,9 +33,6 @@ class StoreCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
|||
@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).'),
|
||||
],
|
||||
|
|
@ -46,7 +43,8 @@ class StoreViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
|
|||
|
||||
schema_tags = ['Stores']
|
||||
permission_classes = [AllowAny]
|
||||
queryset = Store.objects.filter(status=Store.Status.APPROVED).select_related('category', 'city')
|
||||
queryset = Store.objects.filter(status=Store.Status.APPROVED).select_related('category', 'city').distinct()
|
||||
filterset_class = StoreFilter
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve':
|
||||
|
|
@ -56,18 +54,6 @@ class StoreViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
|
|||
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:
|
||||
|
|
@ -76,7 +62,7 @@ class StoreViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.Ge
|
|||
distance=Distance('location', point)
|
||||
).order_by('distance')
|
||||
|
||||
return queryset.distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class SellerStoreView(APIView):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue