From 93fad8036a9b8c07672f6ac36ef2f9e4630bee9d Mon Sep 17 00:00:00 2001 From: Abhinav Desor Date: Thu, 4 May 2017 00:11:42 +0530 Subject: [PATCH 1/2] Add value transformation logic --- filters/mixins.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/filters/mixins.py b/filters/mixins.py index 1dfd840..0066744 100644 --- a/filters/mixins.py +++ b/filters/mixins.py @@ -26,6 +26,7 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): if getattr(self, 'filter_mappings', None) and query_params: filter_mappings = self.filter_mappings + value_transformations = getattr(self, 'filter_value_transformations', {}) try: # check and raise 400_BAD_REQUEST for invalid query params @@ -46,15 +47,16 @@ def __get_queryset_filters(self, query_params, *args, **kwargs): # [1] ~ sign is used to exclude a filter. is_exclude = '~' in query if query in self.filter_mappings and value: - query = filter_mappings[query] + query_filter = filter_mappings[query] + transform_value = value_transformations.get(query, lambda val: val) + transformed_value = transform_value(value) # [2] multiple options is filter values will execute as `IN` query - if isinstance(value, list): - query += '__in' - if value: - if is_exclude: - excludes.append((query, value)) - else: - filters.append((query, value)) + if isinstance(value, list) and not query_filter.endswith('__in'): + query_filter += '__in' + if is_exclude: + excludes.append((query_filter, transformed_value)) + else: + filters.append((query_filter, transformed_value)) return dict(filters), dict(excludes) From 62d3e11f62be42c9d4ef6931861b38bfc4e3721a Mon Sep 17 00:00:00 2001 From: Abhinav Desor Date: Thu, 4 May 2017 18:11:15 +0530 Subject: [PATCH 2/2] Update docs for value transformations --- README.md | 13 ++++- docs/README.md | 139 +------------------------------------------------ 2 files changed, 13 insertions(+), 139 deletions(-) mode change 100644 => 120000 docs/README.md diff --git a/README.md b/README.md index 6bfeca8..8505a3c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ installed using python-pip like `pip install drf-url-filters`. 2. Add `filters` in INSTALLED_APPS of settings.py file of django project. -**How it works?** +**How it works** 1. Your View or ModelViewSet should inherit `FiltersMixin` from `filters.mixins.FiltersMixin`. @@ -26,6 +26,11 @@ installed using python-pip like `pip install drf-url-filters`. have a dict mapping `filter_mappings` which converts incoming query parameters to query you want to make on the column name on the queryset. +3. Optionally, to perform any preprocessing on the incoming values for +query params, add another dict `filter_value_transformations` which maps +incoming query parameters to functions that should be applied to the values +corresponding to them. The resultant value is used in the final filtering. + # validations.py ```python @@ -46,6 +51,7 @@ players_query_schema = base_query_param_schema.extend( "team_id": CSVofIntegers(), # /?team_id=1,2,3 "install_ts": DatetimeWithTZ(), "update_ts": DatetimeWithTZ(), + "taller_than": IntegerLike(), } ) ``` @@ -88,6 +94,11 @@ class PlayersViewSet(FiltersMixin, viewsets.ModelViewSet): 'update_ts': 'update_ts', 'update_ts__gte': 'update_ts__gte', 'update_ts__lte': 'update_ts__lte', + 'taller_than': 'height__gte', + } + + field_value_transformations = { + 'taller_than': lambda val: val / 30.48 # cm to ft } # add validation on filters diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index c002c24..0000000 --- a/docs/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# drf-url-filters - -**drf-url-filters** is a simple django app to apply filters on drf -modelviewset's queryset in a clean, simple and configurable way. It also -supports validations on incoming query params and their values. A beautiful -python package [voluptouos](https://github.com/alecthomas/voluptuous) is being -used for validations on the incoming query parameters. The best part about -voluptouos is you can define your own validations as per your query params -requirements. - -# Quick start ---- -**Installation** - -1. Download `drf-url-filters` app package from this git repo or can be -installed using python-pip like `pip install drf-url-filters`. - -2. Add `filters` in INSTALLED_APPS of settings.py file of django project. - -**How it works?** - -1. Your View or ModelViewSet should inherit `FiltersMixin` from -`filters.mixins.FiltersMixin`. - -2. To apply filters using `drf-url-filters` we need to configure our view to -have a dict mapping `filter_mappings` which converts incoming query parameters -to query you want to make on the column name on the queryset. - -# validations.py - -```python -import six - -from filters.schema import base_query_params_schema -from filters.validations import ( - CSVofIntegers, - IntegerLike, - DatetimeWithTZ -) - -# make a validation schema for players filter query params -players_query_schema = base_query_param_schema.extend( - { - "id": IntegerLike(), - "name": six.text_type, # Depends on python version - "team_id": CSVofIntegers(), # /?team_id=1,2,3 - "install_ts": DatetimeWithTZ(), - "update_ts": DatetimeWithTZ(), - } -) -``` - -# views.py - -```python - -from rest_framework import ( - viewsets, - filters, -) - -from .models import Player, Team -from .pagination import ResultSetPagination -from .serializers import PlayerSerializer, TeamSerializer -from .validations import teams_query_schema, players_query_schema -from filters.mixins import ( - FiltersMixin, -) - - -class PlayersViewSet(FiltersMixin, viewsets.ModelViewSet): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - `update` and `destroy` actions. - """ - serializer_class = PlayerSerializer - pagination_class = ResultSetPagination - filter_backends = (filters.OrderingFilter,) - ordering_fields = ('id', 'name', 'update_ts') - ordering = ('id',) - - # add a mapping of query_params to db_columns(queries) - filter_mappings = { - 'id': 'id', - 'name': 'name__icontains', - 'team_id': 'teams', - 'install_ts': 'install_ts', - 'update_ts': 'update_ts', - 'update_ts__gte': 'update_ts__gte', - 'update_ts__lte': 'update_ts__lte', - } - - # add validation on filters - filter_validation_schema = players_query_schema - - def get_queryset(self): - """ - Optionally restricts the queryset by filtering against - query parameters in the URL. - """ - query_params = self.request.query_params - url_params = self.kwargs - - # get queryset_filters from FilterMixin - queryset_filters = self.get_db_filters(url_params, query_params) - - # This dict will hold filter kwargs to pass in to Django ORM calls. - db_filters = queryset_filters['db_filters'] - - # This dict will hold exclude kwargs to pass in to Django ORM calls. - db_excludes = queryset_filters['db_excludes'] - - # fetch queryset from Players model - queryset = Player.objects.prefetch_related( - 'teams' # use prefetch_related to minimize db hits. - ).all() - - return queryset.filter(**db_filters).exclude(**db_excludes) -``` - -With the use of `drf-url-filters` adding a new filter on a new column is as -simple as adding a new key in the dict. Prohibitting a filter on particular -column is same as removing a key value mapping from the `filter_mappings` dict. - - -# LICENSE -[MIT License](LICENSE.MD) -Copyright (c) 2016 Manjit Kumar. - -# Credits -Special thanks to authors of -[voluptouos](https://github.com/alecthomas/voluptuous) and friends -[cdax](https://github.com/cdax) and [saurabhjha](https://github.com/SaurabhJha) -who encourage people to contribute into open source community. - -# Support -Please [open an issue] -(https://github.com/manjitkumar/drf-url-filters/issues/new) for support. diff --git a/docs/README.md b/docs/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file