.. _filters: Filters ========== **Filters** are used to filter the *queryset* of your service. You can use built-in filters from `django filters `_ or create your own filters. Description ----------- This page will explain how to set up filters in your app services. Filter behave the same as `DRF filters `_. We will reproduce an DRF example and demonstrate how to use `django filters `_ in DSG. Filtering against the current user (context.user) -------------------------------------------------- You might want to filter the *queryset* to ensure that only results relevant to the currently authenticated user making the request are returned. You can do so by filtering based on the value of context.user. For example: .. code-block:: python #quickstart/services.py from django_socio_grpc import generics from quickstart.models import Post from quickstart.serializer import PostProtoSerializer # This service will have all the CRUD actions class PostService(generics.AsyncModelService): queryset = Post.objects.all() serializer_class = PostProtoSerializer def get_queryset(self): """ This view should return a list of all the posts for the currently authenticated user. """ user = self.context.user return Post.objects.filter(user=user) Filtering against request fields (request.user) ------------------------------------------------ Another style of filtering might involve restricting the *queryset* based on some fields of the request. For example with a message like: .. code-block:: proto message PostListRequest { string user = 1; // user here is the id/uuid of the user } .. code-block:: python #quickstart/services.py from django_socio_grpc import generics from quickstart.models import Post from quickstart.serializer import PostProtoSerializer # This service will have all the CRUD actions class PostService(generics.AsyncModelService): queryset = Post.objects.all() serializer_class = PostProtoSerializer def get_queryset(self): """ This view should return a list of all the posts for the currently authenticated user. """ user = self.request.user return Post.objects.filter(user=user) Filtering against metadata --------------------------- A final example of filtering the initial queryset would be to use the `grpc metadata `_ In DSG, metadata is used to replace the query parameters systems. It is very flexible as it's not specified through the proto file. The main inconveniences are: * The metadata are not binary serialized so passing a lot of data as filters may result in **poor performance** * They not exported in the proto so **not documented** by default. .. note:: We are currently looking for filtering best practices. See https://github.com/socotecio/django-socio-grpc/issues/247. .. code-block:: python # server # quickstart/services.py from django_socio_grpc import generics from quickstart.models import Post from quickstart.serializer import PostProtoSerializer # This service will have all the CRUD actions class PostService(generics.AsyncModelService): queryset = Post.objects.all() serializer_class = PostProtoSerializer def get_queryset(self): """ This view should return a list of all the posts for the currently authenticated user. """ user = self.context.grpc_request_metadata["filters"]["user"] # Next line also working to make REST library working # user = self.context.query_params["user"] return Post.objects.filter(user=user) # client import asyncio import grpc import json async def main(): async with grpc.aio.insecure_channel("localhost:50051") as channel: quickstart_client = quickstart_pb2_grpc.PostControllerStub(channel) request = quickstart_pb2.PostListRequest() filter_as_dict = {"user": "be76adbb-73c3-4d65-b823-66b3276df38b"} # json.dumps is used to serialize the dict in the right string format for improved syntax checking metadata = (("filters", (json.dumps(filter_as_dict))),) response = await quickstart_client.List(request, metadata=metadata) if __name__ == "__main__": asyncio.run(main()) DjangoFilterBackend ------------------- First install `django_filters `_. You can also read their `doc for the DRF integration if you are not familiar with it `_. ============================ Register DjangoFilterBackend ============================ You can see a fully working example `in DSG example repo `_. You can register it **by service** or **globally**: * Register DjangoFilterBackend **by service**: .. code-block:: python # quickstart/services.py from django_socio_grpc import generics from django_filters.rest_framework import DjangoFilterBackend class PostService(generics.AsyncModelService): ... filter_backends = [DjangoFilterBackend] * Register DjangoFilterBackend **globally**. :ref:`See DSG settings DEFAULT_FILTER_BACKENDS` * Choose you prefered filtered options. Think about regenerating your proto when changing it. :ref:`See DSG settings FILTER_BEHAVIOR` .. code-block:: python from django_socio_grpc.settings import FilterAndPaginationBehaviorOptions # settings.py GRPC_FRAMEWORK = { ... "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], "FILTER_BEHAVIOR": FilterAndPaginationBehaviorOptions.METADATA_AND_REQUEST_STRUCT ... } ====================== Declare filter fields ====================== There is two way to defining filter fields. * Using ``filterset_fields`` service attribute .. code-block:: python # server # quickstart/services.py from django_socio_grpc import generics from django_filters.rest_framework import DjangoFilterBackend from quickstart.models import Post from quickstart.serializer import PostProtoSerializer # This service will have all the CRUD actions class PostService(generics.AsyncModelService): queryset = Post.objects.all() serializer_class = PostProtoSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ['user'] * Using ``filterset_class`` service attribute. See `here for more details `_ .. code-block:: python # server # quickstart/services.py from django_socio_grpc import generics from django_filters.rest_framework import DjangoFilterBackend from quickstart.models import Post from quickstart.serializer import PostProtoSerializer from django_filters import rest_framework as filters class PostFilter(filters.FilterSet): user = filters.UUIDFilter(field_name="user") class Meta: model = Post fields = ['user'] # This service will have all the CRUD actions class PostService(generics.AsyncModelService): queryset = Post.objects.all() serializer_class = PostProtoSerializer filter_backends = [DjangoFilterBackend] filterset_class = PostFilter ============================================= Add filter field in request for custom action ============================================= If your :ref:`FILTER_BEHAVIOR setting` is set to ``REQUEST_STRUCT_STRICT`` or ``METADATA_AND_REQUEST_STRUCT`` and you want to use filtering for your custom action by message and not metadata (:ref:`See Using It section `) you need to use the :func:`FilterGenerationPlugin` as demonstrated below (:ref:`See Generation Plugin documentation `): .. code-block:: python # server # quickstart/services.py from django_socio_grpc import generics from quickstart.models import Post from quickstart.serializer import PostProtoSerializer from rest_framework.pagination import PageNumberPagination from django_socio_grpc.decorators import grpc_action from django_socio_grpc.protobuf.generation_plugin import ListGenerationPlugin, FilterGenerationPlugin # This service will have all the CRUD actions class PostService(generics.GenericService): queryset = Post.objects.all() serializer_class = PostProtoSerializer pagination_class = PageNumberPagination @grpc_action( request=[], response=PostProtoSerializer, use_generation_plugins=[ListGenerationPlugin(request=True), FilterGenerationPlugin()], ) async def CustomListWithFilter(self, request, context): queryset = self.filter_queryset(self.get_queryset()) serializer = self.get_serializer(queryset, many=True) return serializer.message .. _filters-using-it: ======== Using it ======== You can use metadata or ``_filters`` request field to make the filters work out of the box. For more example you can see the `client in DSG example repo `_ .. code-block:: python # client import asyncio import grpc import json async def main(): ###################################################################################################### # Working if FILTER_BEHAVIOR settings is equal to "METADATA_STRICT" or "METADATA_AND_REQUEST_STRUCT" # ###################################################################################################### async with grpc.aio.insecure_channel("localhost:50051") as channel: quickstart_client = quickstart_pb2_grpc.PostControllerStub(channel) request = quickstart_pb2.PostListRequest() # filters only the user with id "be76adbb-73c3-4d65-b823-66b3276df38b" filter_as_dict = {"user": "be76adbb-73c3-4d65-b823-66b3276df38b"} metadata = (("filters", (json.dumps(filter_as_dict))),) response = await quickstart_client.List(request, metadata=metadata) ############################################################################################################ # Working if FILTER_BEHAVIOR settings is equal to "REQUEST_STRUCT_STRICT" or "METADATA_AND_REQUEST_STRUCT" # ############################################################################################################ async with grpc.aio.insecure_channel("localhost:50051") as channel: quickstart_client = quickstart_pb2_grpc.PostControllerStub(channel) # filters only the user with id "be76adbb-73c3-4d65-b823-66b3276df38b" filter_as_dict = {"user": "be76adbb-73c3-4d65-b823-66b3276df38b"} filter_as_struct = struct_pb2.Struct() filter_as_struct.update(filter_as_dict) # _filters field is only generated if you set FILTER_BEHAVIOR to the correct options. Think to regenerate proto after changing it. request = quickstart_pb2.PostListRequest(_filters=filter_as_struct) response = await quickstart_client.List(request) if __name__ == "__main__": asyncio.run(main()) For web usage see :ref:`How to web: Using js client` SearchFilter ------------- DSG also supports the `DRF SearchFilter `_ Refer to the DRF docs for implementation details and specific lookup. .. code-block:: python # server # quickstart/services.py from django_socio_grpc import generics from rest_framework import filters from quickstart.models import Post from quickstart.serializer import PostProtoSerializer # This service will have all the CRUD actions class PostService(generics.AsyncModelService): queryset = Post.objects.all() serializer_class = PostProtoSerializer filter_backends = [filters.SearchFilter] search_fields = ['user__full_name'] # client import asyncio import grpc import json async def main(): ###################################################################################################### # Working if FILTER_BEHAVIOR settings is equal to "METADATA_STRICT" or "METADATA_AND_REQUEST_STRUCT" # ###################################################################################################### async with grpc.aio.insecure_channel("localhost:50051") as channel: quickstart_client = quickstart_pb2_grpc.PostControllerStub(channel) request = quickstart_pb2.PostListRequest() filter_as_dict = {"search": "test-user"} # search for "test-user" in user__full_name metadata = (("filters", (json.dumps(filter_as_dict))),) response = await quickstart_client.List(request, metadata=metadata) ############################################################################################################ # Working if FILTER_BEHAVIOR settings is equal to "REQUEST_STRUCT_STRICT" or "METADATA_AND_REQUEST_STRUCT" # ############################################################################################################ async with grpc.aio.insecure_channel("localhost:50051") as channel: quickstart_client = quickstart_pb2_grpc.PostControllerStub(channel) filter_as_dict = {"search": "test-user"} filter_as_struct = struct_pb2.Struct() filter_as_struct.update(filter_as_dict) # _filters field is only generated if you set FILTER_BEHAVIOR to the correct options. Think to regenerate proto after changing it. request = quickstart_pb2.PostListRequest(_filters=filter_as_struct) response = await quickstart_client.List(request) if __name__ == "__main__": asyncio.run(main()) OrderingFilter -------------- OrderingFilters are used to control the ordering of the results. DSG also support the `DRF OrderingFilter `_. Refer to the `DRF doc `_ for implementation details and specific lookup. But as the DRF OrderingFilter only accepts string for ordering (`ordering=-field1,field2`) while gRPC is able to use arrays we provide our own :func:`OrderingFilter` that supports it. .. code-block:: python # server # quickstart/services.py from django_socio_grpc import generics, filters from quickstart.models import Post from quickstart.serializer import PostProtoSerializer # This service will have all the CRUD actions class PostService(generics.AsyncModelService): queryset = Post.objects.all() serializer_class = PostProtoSerializer filter_backends = [filters.OrderingFilter] ordering_fields = ['pub_date'] # client import asyncio import grpc import json async def main(): ###################################################################################################### # Working if FILTER_BEHAVIOR settings is equal to "METADATA_STRICT" or "METADATA_AND_REQUEST_STRUCT" # ###################################################################################################### async with grpc.aio.insecure_channel("localhost:50051") as channel: quickstart_client = quickstart_pb2_grpc.PostControllerStub(channel) request = quickstart_pb2.PostListRequest() # order by descending pub_date filter_as_dict = {"ordering": "-pub_date"} # or {"ordering": ["-pub_date"]} if multiple ordering and using DSG OrderingFilter metadata = (("filters", (json.dumps(filter_as_dict))),) response = await quickstart_client.List(request, metadata=metadata) ############################################################################################################ # Working if FILTER_BEHAVIOR settings is equal to "REQUEST_STRUCT_STRICT" or "METADATA_AND_REQUEST_STRUCT" # ############################################################################################################ async with grpc.aio.insecure_channel("localhost:50051") as channel: quickstart_client = quickstart_pb2_grpc.PostControllerStub(channel) # filters only the user with id "be76adbb-73c3-4d65-b823-66b3276df38b" filter_as_dict = {"ordering": "-pub_date"} # or {"ordering": ["-pub_date"]} if multiple ordering and using DSG OrderingFilter filter_as_struct = struct_pb2.Struct() filter_as_struct.update(filter_as_dict) # _filters field is only generated if you set FILTER_BEHAVIOR to the correct options. Think to regenerate proto after changing it. request = quickstart_pb2.PostListRequest(_filters=filter_as_struct) response = await quickstart_client.List(request) if __name__ == "__main__": asyncio.run(main()) .. _filters-web-usage: Web Example ----------- For web usage of the client see :ref:`How to web: Using JS client` .. code-block:: javascript import { Struct } from "@bufbuild/protobuf"; // See web usage to understand how to use the client. const postClient = createPromiseClient(PostController, transport); // filters only the user with id 1 const filtersStruct = Struct.fromJson({user: 1}); const res = await postClient.list({ Filters: filtersStruct }); // _filters is transformed to Filters in buf build used by connect console.log(res) // filters only the users with username containing "test-user" const filtersStruct = Struct.fromJson({search: "test-user"}); const res = await postClient.list({ Filters: filtersStruct }); // _filters is transformed to Filters in buf build used by connect console.log(res) .. warning:: The following example is the deprecated way of using filters. Please use the example above. Note that the example works depending on the `metadata` :ref:`FILTER_BEHAVIOR setting` settings. .. code-block:: javascript // See web usage to understand how to use the client. const postClient = createPromiseClient(PostController, transport); // filters only the user with id 1 headers = {filters: JSON.stringify({user: 1})} const res = await postClient.list({}, {headers}) console.log(res) // filters only the users with username containing "test-user" headers = {filters: JSON.stringify({search: "test-user"})} const res = await postClient.list({}, {headers}) console.log(res)