diff --git a/backend/cfehome/cfehome/settings.py b/backend/cfehome/cfehome/settings.py index 7fe2b79..6399eef 100644 --- a/backend/cfehome/cfehome/settings.py +++ b/backend/cfehome/cfehome/settings.py @@ -34,6 +34,7 @@ 'rest_framework.authtoken', 'products.apps.ProductsConfig', 'users.apps.UsersConfig', + 'search.apps.SearchConfig', ] MIDDLEWARE = [ diff --git a/backend/cfehome/cfehome/test_settings.py b/backend/cfehome/cfehome/test_settings.py index b160fd7..4787e5a 100644 --- a/backend/cfehome/cfehome/test_settings.py +++ b/backend/cfehome/cfehome/test_settings.py @@ -20,6 +20,7 @@ 'rest_framework.authtoken', 'products.apps.ProductsConfig', 'users.apps.UsersConfig', + 'search.apps.SearchConfig', ] MIDDLEWARE = [ diff --git a/backend/cfehome/products/index.py b/backend/cfehome/products/index.py index 0762a2b..06a6b55 100644 --- a/backend/cfehome/products/index.py +++ b/backend/cfehome/products/index.py @@ -8,5 +8,9 @@ class ProductModelIndex(algoliasearch_django.AlgoliaIndex): """индекс Algolia для модели Product""" - fields = ('title', 'description', 'user') + fields = ('title', 'description', 'user', 'is_public') + settings = { + 'attributesForFaceting': ['user', 'is_public'], + 'searchableAttributes': ['title', 'description'], + } index_name = 'product_index' diff --git a/backend/cfehome/products/models.py b/backend/cfehome/products/models.py index 769b965..07c0294 100644 --- a/backend/cfehome/products/models.py +++ b/backend/cfehome/products/models.py @@ -1,8 +1,7 @@ -import users.models - import django.db.models import products.managers +import users.models class Product(django.db.models.Model): diff --git a/backend/cfehome/products/search_service.py b/backend/cfehome/products/search_service.py deleted file mode 100644 index eb31126..0000000 --- a/backend/cfehome/products/search_service.py +++ /dev/null @@ -1,18 +0,0 @@ -import algoliasearch.search_index -import algoliasearch_django - - -def get_search_index( - index_name: str, -) -> algoliasearch.search_index.SearchIndex: - """возвращаем объект индекса по его названию""" - return algoliasearch_django.algolia_engine.client.init_index(index_name) - - -def perform_search(query: str, search_index: str) -> list[int]: - """поиск id моделей по запросу query и search_index""" - index = get_search_index(search_index) - result_inds = [ - int(item['objectID']) for item in index.search(query)['hits'] - ] - return result_inds diff --git a/backend/cfehome/products/serializers.py b/backend/cfehome/products/serializers.py index 6d6ca9f..e4530b4 100644 --- a/backend/cfehome/products/serializers.py +++ b/backend/cfehome/products/serializers.py @@ -1,8 +1,8 @@ import rest_framework.reverse import rest_framework.serializers -import users.serializers import products.models +import users.serializers class ProductListSerializer(rest_framework.serializers.ModelSerializer): @@ -11,10 +11,11 @@ class ProductListSerializer(rest_framework.serializers.ModelSerializer): url = rest_framework.serializers.HyperlinkedIdentityField( view_name='products:product-detail' ) + user = users.serializers.UserPublicSerializer(read_only=True) class Meta: model = products.models.Product - fields = ['pk', 'url', 'title', 'price', 'description'] + fields = ['pk', 'url', 'title', 'price', 'description', 'user'] class ProductDetailSerializer(rest_framework.serializers.ModelSerializer): diff --git a/backend/cfehome/products/viewsets.py b/backend/cfehome/products/viewsets.py index fc34e6f..5a1bdeb 100644 --- a/backend/cfehome/products/viewsets.py +++ b/backend/cfehome/products/viewsets.py @@ -7,38 +7,29 @@ import products.models import products.permissions -import products.search_service import products.serializers +import search.mixins +import search.services -class ProductViewSet(rest_framework.viewsets.ModelViewSet): +class ProductViewSet( + search.mixins.ListSearchMixin, rest_framework.viewsets.ModelViewSet +): """вьюсет для модели Product""" queryset = products.models.Product.objects.all() - - def get_queryset(self) -> django.db.models.QuerySet: - """ - получаем queryset в зависимости от - поискового запроса и пользователя - """ - if self.action == 'list': - user_pk = None - if self.request.user.is_authenticated: - user_pk = self.request.user.pk - user_based_queryset = ( - products.models.Product.objects.search_by_user(user_pk) - ) - - query = self.request.GET.get('query', '') - if not query: - return user_based_queryset - - return user_based_queryset.filter( - pk__in=products.search_service.perform_search( - query, 'product_index' - ) - ) - return self.__class__.queryset + availabe_search_params = {'user': str, 'is_public': lambda val: val == '1'} + index_name = 'product_index' + + def get_default_queryset(self) -> django.db.models.QuerySet: + """получаем базовый queryset для поиска по нему""" + user_pk = None + if self.request.user.is_authenticated: + user_pk = self.request.user.pk + user_based_queryset = ( + products.models.Product.objects.search_by_user(user_pk) + ).select_related('user') + return user_based_queryset def get_serializer_class(self) -> rest_framework.serializers.Serializer: """получаем serializer для запроса""" @@ -59,9 +50,3 @@ def get_permissions(self) -> list: products.permissions.RetrieveUpdateDestroyProductPermission ] return [permission() for permission in permission_classes] - - # def list( - # self, request: django.http.HttpRequest, *args, **kwargs - # ) -> django.http.HttpResponse: - # """список элементов""" - # return super().list(request, *args, **kwargs) diff --git a/backend/cfehome/search/__init__.py b/backend/cfehome/search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/cfehome/search/apps.py b/backend/cfehome/search/apps.py new file mode 100644 index 0000000..6199c87 --- /dev/null +++ b/backend/cfehome/search/apps.py @@ -0,0 +1,9 @@ +import django.apps + + +class SearchConfig(django.apps.AppConfig): + """базовый класс приложения Search""" + + default_auto_field = 'django.db.models.BigAutoField' + name = 'search' + verbose_name = 'поиск' diff --git a/backend/cfehome/search/mixins.py b/backend/cfehome/search/mixins.py new file mode 100644 index 0000000..74b5d35 --- /dev/null +++ b/backend/cfehome/search/mixins.py @@ -0,0 +1,41 @@ +import abc +import typing + +import django.db.models + +import search.services + + +class ListSearchMixin: + """миксин для посика по списку""" + + availabe_search_params = dict[str, typing.Callable] + index_name: str + + @abc.abstractmethod + def get_default_queryset(self) -> django.db.models.QuerySet: + """базовый queryset для поиска""" + ... + + def get_queryset(self) -> django.db.models.QuerySet: + """ + получаем queryset в зависимости от поискового запроса + """ + if self.action == 'list': + default_queryset = self.get_default_queryset() + + query = self.request.GET.get('query', '') + + params = search.services.extract_search_params( + self.request.GET, self.availabe_search_params + ) + + if not params and not query: + return default_queryset + + return default_queryset.filter( + pk__in=search.services.get_results_ids( + query, self.index_name, **params + ) + ) + return self.__class__.queryset diff --git a/backend/cfehome/search/services.py b/backend/cfehome/search/services.py new file mode 100644 index 0000000..1a4569b --- /dev/null +++ b/backend/cfehome/search/services.py @@ -0,0 +1,45 @@ +import typing + +import algoliasearch.search_index +import algoliasearch_django + + +def extract_search_params( + request_get: dict, available_params: dict[str, typing.Callable] +) -> dict: + """получаем параметры поиска из словаря request.GET""" + params = {} + for param in available_params: + val = request_get.get(param, '') + if val: + params[param] = available_params[param](val) + return params + + +def get_search_index( + index_name: str, +) -> algoliasearch.search_index.SearchIndex: + """возвращаем объект индекса по его названию""" + return algoliasearch_django.algolia_engine.client.init_index(index_name) + + +def get_search_results(query: str, search_index: str, **kwargs) -> dict: + """ + получаем ответ от algolia в виде словаря + по запросу query и search_index + """ + search_params = {} + kwargs_filters = [f'{k}:{v}' for k, v in kwargs.items() if v] + if kwargs_filters: + search_params['facetFilters'] = kwargs_filters + index = get_search_index(search_index) + return index.search(query, search_params) + + +def get_results_ids(query: str, search_index: str, **kwargs) -> list[int]: + """получаем id записей по запросу query и search_index""" + result_inds = [ + int(item['objectID']) + for item in get_search_results(query, search_index, **kwargs)['hits'] + ] + return result_inds diff --git a/backend/cfehome/users/admin.py b/backend/cfehome/users/admin.py index 4bfd455..9c10a0f 100644 --- a/backend/cfehome/users/admin.py +++ b/backend/cfehome/users/admin.py @@ -1,8 +1,8 @@ -import users.models - import django.contrib.admin import django.contrib.auth.admin +import users.models + class UserAdmin(django.contrib.auth.admin.UserAdmin): """отображение модели пользователя в админке""" diff --git a/backend/cfehome/users/models.py b/backend/cfehome/users/models.py index 6f00fdc..ddc877c 100644 --- a/backend/cfehome/users/models.py +++ b/backend/cfehome/users/models.py @@ -1,7 +1,7 @@ -import users.managers - import django.contrib.auth.models +import users.managers + class User(django.contrib.auth.models.AbstractUser): """кастомный пользователь""" diff --git a/pyproject.toml b/pyproject.toml index b845577..3d38d92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ extend-exclude=''' [tool.isort] default_section = "THIRDPARTY" known_django = "django" -known_local_folder = ["cfehome", "api", "products"] +known_local_folder = ["cfehome", "products", "search", "users"] sections = ["FUTURE","STDLIB","THIRDPARTY","DJANGO","FIRSTPARTY","LOCALFOLDER"] skip = [".gitignore", "venv", "env"] skip_glob = ["*/migrations/*"]