Skip to content

Commit

Permalink
Switch search fields to list; closes #48
Browse files Browse the repository at this point in the history
  • Loading branch information
stevelacey committed Oct 26, 2021
1 parent 49418ec commit 95c0421
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 64 deletions.
40 changes: 17 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,41 +158,36 @@ Class Attributes

|name | required | type | description |
| -- | -- | -- | -- |
|`bundle_name` | no | `str` | _Default:_ `None` If set, the returned data will use this name. I.e., {`bundle_name`: return_data}|
|`model`| yes | `class` | An uninstantiated `django.db.models.model` class. |
|`permissions`| yes | `list` of permissions classes | Will return appropriate HTTP status code based on the definition of the permission class.
| | | | |
|`bundle_name` | no | `str` | _Default:_ `None` If set, the returned data will use this name. I.e., {`bundle_name`: return_data} |
|`model` | yes | `class` | An uninstantiated `django.db.models.model` class. |
|`permissions` | yes | `list` of permissions classes | Will return appropriate HTTP status code based on the definition of the permission class. |

##### HTTP Methods

- GET is always supported.


#### `ListAPI`

|name | required | type | description |
| -- | -- | -- | -- |
|`api_method` | no | `str` | _Default:_ `api`. Must refer to a `model` method. This method is used to return data for `GET` requests. |
|`payload_key` | no | `str` | _Default:_ `model._meta.verbose_name_plural`. Use in order to rename the key for the results array |
|`filters` | no | `dict` | _Default:_ `{}`. Pass key/value pairs that you wish to further filter the queryset beyond the `lookup_url_kwarg` |
|`lookup_field` | no | `str` | Use these two settings in tandem in order to filter `get_queryset` based on a URL field. `lookup_url_kwarg` is required if this is set. |
|`lookup_url_kwarg`| no | `str` | Use these two settings in tandem in order to filter `get_queryset` based on a URL field. `lookup_field` is required if this is set. |
|`ordering` | no | `list` | _Default_: `[]`. Pass a list of fields to default the queryset order by. |
|`filter_fields` | no | `list` | _Default_: `None`. Pass a list of fields to support filtering via query params. |
|`search_fields` | no | `dict` or `bool` | _Default_: `None`. Pass a dict with `and`/`or` fields to search via the `q` query param. |
|`sort_fields` | no | `list` | _Default_: `None`. Pass a list of fields to support sorting via the `sort` query param. |
|`per_page` | no | `int` | _Default_: 25. Sets the number of results returned for each page. |
|`max_per_page` | no | `int` | _Default_: Same as `per_page`. Sets the max number of results to allow when passing the `perPage` query param. |
| | | | |
|`api_method` | no | `str` | _Default:_ `api`. Must refer to a `model` method. This method is used to return data for `GET` requests. |
|`payload_key` | no | `str` | _Default:_ `model._meta.verbose_name_plural`. Use in order to rename the key for the results array |
|`filters` | no | `dict` | _Default:_ `{}`. Pass key/value pairs that you wish to further filter the queryset beyond the `lookup_url_kwarg` |
|`lookup_field` | no | `str` | Use these two settings in tandem in order to filter `get_queryset` based on a URL field. `lookup_url_kwarg` is required if this is set. |
|`lookup_url_kwarg` | no | `str` | Use these two settings in tandem in order to filter `get_queryset` based on a URL field. `lookup_field` is required if this is set. |
|`ordering` | no | `list` | _Default_: `[]`. Pass a list of fields to default the queryset order by. |
|`filter_fields` | no | `list` | _Default_: `[]`. Pass a list of fields to support filtering via query params. |
|`search_fields` | no | `list` | _Default_: `[]`. Pass a list of fields to full text search via the `q` query param. |
|`sort_fields` | no | `list` | _Default_: `[]`. Pass a list of fields to support sorting via the `sort` query param. |
|`per_page` | no | `int` | _Default_: 25. Sets the number of results returned for each page. |
|`max_per_page` | no | `int` | _Default_: Same as `per_page`. Sets the max number of results to allow when passing the `perPage` query param. |

##### Search

Setting `search_fields` to `True` will enable search based on url parameters.
Parameters in the URL must be camelCase and exactly match the snake_case model
field.
Parameters in the URL must be camelCase and exactly match the snake_case model field.

To allow full text search, set to a dictionary of two lists: 'or' & 'and'. Each
list will be assembled using either `Q.OR` or `Q.AND`. Fields in each list
must be composed of django filter lookup argument names.
To allow full text search, set to a list of fields for django filter lookups.

##### Pagination

Expand Down Expand Up @@ -233,7 +228,6 @@ properly. Do this by performing whatever logic you need to do, then update the
|`api_method` | `api` | `str` | Must refer to a `model` method. This method is used to return data for `GET` requests. Additionally, `{api_method}_update_fields` will be called to execute `PATCH` requests.|
|`lookup_field`| `id` | `str` | Override with the lookup field used to filter the _model_. Defaults to `id`|
|`lookup_url_kwarg`| `id` | `str` | Override with the name of the parameter passed to the view by the URL route. Defaults to `id` |
| | | | |

##### `get_instance()` method
This method uses `lookup_field` and `lookup_url_kwargs` to return a model instance.
Expand Down
12 changes: 5 additions & 7 deletions tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ProfileList(ListAPI):
ordering = ["pk"]
serializer = ProfileSerializer
permissions = [PublicEndpoint]
search_fields = {}
search_fields = []
filter_fields = [
"tags",
]
Expand All @@ -42,17 +42,15 @@ class UserList(ListAPI):
ordering = ["pk"]
serializer = UserSerializer
permissions = [PublicEndpoint]
search_fields = {
"or": [
"email",
"username",
]
}
filter_fields = [
"email",
"date_joined__gte",
"date_joined__lte",
]
search_fields = [
"email",
"username",
]
sort_fields = [
"id",
"date_joined",
Expand Down
78 changes: 44 additions & 34 deletions worf/views/list.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import operator
from functools import reduce
import warnings

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Paginator, EmptyPage
Expand All @@ -14,11 +18,10 @@ class ListAPI(AbstractBaseAPI):
lookup_url_kwarg = "id" # default incase lookup_field is set
filters = {}
ordering = []
filter_fields = None
search_fields = None
sort_fields = None
filter_fields = []
search_fields = []
sort_fields = []
queryset = None
q_objects = Q()
filter_set = None
count = 0
page_num = 1
Expand All @@ -31,25 +34,35 @@ def get(self, request, *args, **kwargs):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

codepath = self.codepath

if not isinstance(self.filters, dict):
raise ImproperlyConfigured(f"{self.codepath}.filters must be type: dict")
raise ImproperlyConfigured(f"{codepath}.filters must be type: dict")

if not isinstance(self.ordering, list):
raise ImproperlyConfigured(f"{self.codepath}.ordering must be type: list")
if self.filter_fields is not None and not isinstance(self.filter_fields, list):
raise ImproperlyConfigured(
f"{self.codepath}.filter_fields must be type: list"
)
if self.search_fields is not None and not isinstance(self.search_fields, dict):
raise ImproperlyConfigured(
f"{self.codepath}.search_fields must be type: dict"
)
if self.sort_fields is not None and not isinstance(self.sort_fields, list):
raise ImproperlyConfigured(
f"{self.codepath}.sort_fields must be type: list"
)
raise ImproperlyConfigured(f"{codepath}.ordering must be type: list")

if not isinstance(self.filter_fields, list):
raise ImproperlyConfigured(f"{codepath}.filter_fields must be type: list")

if not isinstance(self.search_fields, (dict, list)):
raise ImproperlyConfigured(f"{codepath}.search_fields must be type: list")

if not isinstance(self.sort_fields, list):
raise ImproperlyConfigured(f"{codepath}.sort_fields must be type: list")

# generate a default filterset if a custom one was not provided
if self.filter_set is None:
self.filter_set = generate_filterset(self.model)

# support deprecated search_fields and/or dict syntax (note that `and` does nothing)
if isinstance(self.search_fields, dict):
warnings.warn(
f"Passing a dict to {codepath}.search_fields is deprecated. Pass a list instead."
)
self.search_fields = self.search_fields.get("or", [])

def _set_base_lookup_kwargs(self):
# Filters set directly on the class
self.lookup_kwargs.update(self.filters)
Expand All @@ -69,27 +82,24 @@ def set_search_lookup_kwargs(self):
For more advanced search use cases, override this method and pass GET
with any remaining params you want to use classic django filters for.
"""
if self.search_fields is None:
"""If self.search_fields is not set, we don't allow search."""
if not self.filter_fields and not self.search_fields:
return

self.set_bundle_from_querystring()

# Whatever is not q or page as a querystring param will
# be used for key-value search.
search_string = self.bundle.get("q", False)
query = self.bundle.pop("q", "").strip()

self.bundle.pop("page", None)
self.bundle.pop("p", None)
self.bundle.pop("q", None)

if search_string and len(search_string):
the_ors = self.search_fields.get("or", [])
for attr in the_ors:
kwarg = {attr + "__icontains": search_string.strip()}
self.q_objects.add(Q(**kwarg), Q.OR)

the_ands = self.search_fields.get("ands", [])
for attr in the_ands:
kwarg = {attr + "__icontains": search_string.strip()}
if query:
search_icontains = (
Q(**{f"{search_field}__icontains": query})
for search_field in self.search_fields
)
self.search_query = reduce(operator.or_, search_icontains)

if not self.filter_fields or not self.bundle:
return
Expand Down Expand Up @@ -131,7 +141,7 @@ def get_processed_queryset(self):
queryset = self.get_queryset()

self.lookup_kwargs = {}
self.q_objects = Q()
self.search_query = Q()

self._set_base_lookup_kwargs()
self.set_search_lookup_kwargs()
Expand All @@ -145,7 +155,7 @@ def get_processed_queryset(self):
try:
queryset = (
apply_filterset(self.filter_set, queryset, self.lookup_kwargs)
.filter(self.q_objects)
.filter(self.search_query)
.order_by(*order_by)
.distinct()
)
Expand Down Expand Up @@ -229,7 +239,7 @@ def serialize(self):
"bundle": self.bundle,
"lookup_kwargs": self.lookup_kwargs,
"query": self.query,
"q_objs": str(self.q_objects),
"search_query": str(self.search_query),
"serializer": str(serializer),
}
}
Expand Down

0 comments on commit 95c0421

Please sign in to comment.