diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c04a3..d94057f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/apps/locations/tests/test_location_list.py b/apps/locations/tests/test_location_list.py new file mode 100644 index 0000000..c571b44 --- /dev/null +++ b/apps/locations/tests/test_location_list.py @@ -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)}) diff --git a/apps/locations/views.py b/apps/locations/views.py index f156533..0b9b06f 100644 --- a/apps/locations/views.py +++ b/apps/locations/views.py @@ -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