Add filters to the /v1/stories/search endpoint

This commit adds almost the same set of parameters that the /v1/stories
endpoint supports to the /v1/stories/search endpoint. This allows one
to obtain some search results and then filter them more easily than
before, as well as avoiding a bug when trying to naively search for
stories without removing the default criteria in the web ui.

The title and description filters are not added, as they don't make
much sense in the context of a fulltext search.

This commit introduces some code duplication, and long term it would
probably be best to merge the two endpoints into one and do the fulltext
search iff the q parameter is given, rather than trying to maintain
two functions with the same set of parameters for no reason.

Change-Id: Ibbe1ed38094fb1d02919cf97ed311cf79af049bb
This commit is contained in:
Adam Coldrick 2016-10-22 10:24:09 +00:00 committed by Adam Coldrick
parent db050a3de6
commit 4aed46c189
3 changed files with 99 additions and 30 deletions

View File

@ -19,6 +19,7 @@ import sqlalchemy_fulltext.modes as FullTextMode
from storyboard.api.v1.search import search_engine
from storyboard.db.api import base as api_base
from storyboard.db.api import stories as stories_api
from storyboard.db import models
@ -30,7 +31,12 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
return query
def _apply_pagination(self, model_cls, query, marker=None,
offset=None, limit=None):
offset=None, limit=None, sort_field='id',
sort_dir='asc'):
if not sort_field:
sort_field = 'id'
if not sort_dir:
sort_dir = 'asc'
marker_entity = None
if marker:
@ -53,27 +59,58 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
return query.all()
def stories_query(self, q, marker=None, offset=None,
limit=None, current_user=None, **kwargs):
def stories_query(self, q, status=None, assignee_id=None,
creator_id=None, project_group_id=None, project_id=None,
subscriber_id=None, tags=None, updated_since=None,
marker=None, offset=None,
limit=None, tags_filter_type="all", sort_field='id',
sort_dir='asc', current_user=None):
session = api_base.get_session()
subquery = api_base.model_query(models.Story, session)
subquery = stories_api._story_build_query(
assignee_id=assignee_id,
creator_id=creator_id,
project_group_id=project_group_id,
project_id=project_id,
tags=tags,
updated_since=updated_since,
tags_filter_type=tags_filter_type,
session=session)
# Filter by subscriber ID
if subscriber_id is not None:
subs = api_base.model_query(models.Subscription)
subs = api_base.apply_query_filters(query=subs,
model=models.Subscription,
target_type='story',
user_id=subscriber_id)
subs = subs.subquery()
subquery = subquery.join(subs, subs.c.target_id == models.Story.id)
subquery = self._build_fulltext_search(models.Story, subquery, q)
subquery = self._apply_pagination(models.Story,
subquery, marker, offset, limit)
# Filter out stories that the current user can't see
subquery = api_base.filter_private_stories(subquery, current_user)
subquery = subquery.subquery('stories_with_idx')
query = api_base.model_query(models.StorySummary)\
.options(subqueryload(models.StorySummary.tags))
query = query.join(subquery,
models.StorySummary.id == subquery.c.id)
models.StorySummary.id == subquery.c.stories_id)
# Filter out stories that the current user can't see
query = api_base.filter_private_stories(query, current_user)
if status:
query = query.filter(models.StorySummary.status.in_(status))
stories = query.all()
return stories
query = self._apply_pagination(models.StorySummary,
query,
marker,
offset,
limit,
sort_field=sort_field,
sort_dir=sort_dir)
return query.all()
def tasks_query(self, q, marker=None, offset=None, limit=None,
current_user=None, **kwargs):

View File

@ -357,9 +357,15 @@ class StoriesController(rest.RestController):
@decorators.db_exceptions
@secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.Story], wtypes.text, wtypes.text,
int, int, int)
def search(self, q="", marker=None, offset=None, limit=None):
@wsme_pecan.wsexpose([wmodels.Story], wtypes.text,
[wtypes.text], int, int, int, int, int, [wtypes.text],
datetime, int, int, int, wtypes.text,
wtypes.text, wtypes.text)
def search(self, q="", status=None, assignee_id=None, creator_id=None,
project_group_id=None, project_id=None, subscriber_id=None,
tags=None, updated_since=None, marker=None, offset=None,
limit=None, tags_filter_type='all', sort_field='id',
sort_dir='asc'):
"""The search endpoint for stories.
Example::
@ -367,15 +373,40 @@ class StoriesController(rest.RestController):
curl https://my.example.org/api/v1/stories/search?q=pep8
:param q: The query string.
:return: List of Stories matching the query.
"""
:param status: Only show stories with this particular status.
:param assignee_id: Filter stories by who they are assigned to.
:param creator_id: Filter stories by who created them.
:param project_group_id: Filter stories by project group.
:param project_id: Filter stories by project ID.
:param subscriber_id: Filter stories by subscriber ID.
:param tags: A list of tags to filter by.
:param updated_since: Filter stories by last updated time.
:param marker: The resource id where the page should begin.
:param offset: The offset to start the page at.
:param limit: The number of stories to retrieve.
:param tags_filter_type: Type of tags filter.
:param sort_field: The name of the field to sort on.
:param sort_dir: Sort direction for results (asc, desc).
:return: List of Stories matching the query and any other filters.
"""
user = request.current_user_id
stories = SEARCH_ENGINE.stories_query(q=q,
marker=marker,
offset=offset,
limit=limit,
current_user=user)
stories = SEARCH_ENGINE.stories_query(
q,
status=status,
assignee_id=assignee_id,
creator_id=creator_id,
project_group_id=project_group_id,
project_id=project_id,
subscriber_id=subscriber_id,
tags=tags,
updated_since=updated_since,
offset=offset,
tags_filter_type=tags_filter_type,
limit=limit,
sort_field=sort_field,
sort_dir=sort_dir,
current_user=user)
return [create_story_wmodel(story) for story in stories]

View File

@ -90,8 +90,10 @@ def story_get_all(title=None, description=None, status=None, assignee_id=None,
project_id=project_id,
tags=tags,
updated_since=updated_since,
tags_filter_type=tags_filter_type,
current_user=current_user)
tags_filter_type=tags_filter_type)
# Filter out stories that the current user can't see
subquery = api_base.filter_private_stories(subquery, current_user)
# Filter by subscriber ID
if subscriber_id is not None:
@ -141,8 +143,10 @@ def story_get_count(title=None, description=None, status=None,
project_id=project_id,
updated_since=updated_since,
tags=tags,
tags_filter_type=tags_filter_type,
current_user=current_user)
tags_filter_type=tags_filter_type)
# Filter out stories that the current user can't see
query = api_base.filter_private_stories(query, current_user)
# Filter by subscriber ID
if subscriber_id is not None:
@ -169,9 +173,9 @@ def story_get_count(title=None, description=None, status=None,
def _story_build_query(title=None, description=None, assignee_id=None,
creator_id=None, project_group_id=None,
project_id=None, updated_since=None, tags=None,
tags_filter_type='all', current_user=None):
tags_filter_type='all', session=None):
# First build a standard story query.
query = api_base.model_query(models.Story.id).distinct()
query = api_base.model_query(models.Story.id, session=session).distinct()
# Apply basic filters
query = api_base.apply_query_filters(query=query,
@ -182,9 +186,6 @@ def _story_build_query(title=None, description=None, assignee_id=None,
if updated_since:
query = query.filter(models.Story.updated_at > updated_since)
# Filter out stories that the current user can't see
query = api_base.filter_private_stories(query, current_user)
# Filtering by tags
if tags:
if tags_filter_type == 'all':