Add search to city/neighborhood endpoints; document filters for swagger

Neighborhood filtering was UUID-only; add name-based search on both
GET /api/v1/cities/ and GET /api/v1/neighborhoods/ (the latter also
matching by city name), and document all params for drf-spectacular.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ali Asadi 2026-08-22 11:25:45 +03:30
parent 661129d0a1
commit fa9a69fff3
3 changed files with 75 additions and 0 deletions

View file

@ -7,5 +7,8 @@
- `GET /api/v1/products/` — same issue: documented the existing `store`, `category`, and `search` query params for swagger (`apps/catalog/views.py`).
### 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`).
- 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.

View file

@ -0,0 +1,42 @@
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)})

View file

@ -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.permissions import AllowAny, IsAuthenticated
@ -7,13 +9,35 @@ 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
@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]
@ -22,9 +46,15 @@ class NeighborhoodViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, view
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