Merge "Support offset/limit pagination in the API"

This commit is contained in:
Jenkins 2015-07-10 14:01:34 +00:00 committed by Gerrit Code Review
commit 6486c9482c
9 changed files with 88 additions and 40 deletions

View File

@ -115,13 +115,14 @@ class ProjectGroupsController(rest.RestController):
@decorators.db_exceptions @decorators.db_exceptions
@secure(checks.guest) @secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.ProjectGroup], int, int, wtypes.text, @wsme_pecan.wsexpose([wmodels.ProjectGroup], int, int, int, wtypes.text,
wtypes.text, wtypes.text, wtypes.text) wtypes.text, wtypes.text, wtypes.text)
def get(self, marker=None, limit=None, name=None, title=None, def get(self, marker=None, offset=None, limit=None, name=None, title=None,
sort_field='id', sort_dir='asc'): sort_field='id', sort_dir='asc'):
"""Retrieve a list of projects groups. """Retrieve a list of projects groups.
:param marker: The resource id where the page should begin. :param marker: The resource id where the page should begin.
:param offset: The offset to start the page at.
:param limit: The number of project groups to retrieve. :param limit: The number of project groups to retrieve.
:param name: A string to filter the name by. :param name: A string to filter the name by.
:param title: A string to filter the title by. :param title: A string to filter the title by.
@ -133,9 +134,12 @@ class ProjectGroupsController(rest.RestController):
limit = max(0, limit) limit = max(0, limit)
# Resolve the marker record. # Resolve the marker record.
marker_group = None
if marker is not None:
marker_group = project_groups.project_group_get(marker) marker_group = project_groups.project_group_get(marker)
groups = project_groups.project_group_get_all(marker=marker_group, groups = project_groups.project_group_get_all(marker=marker_group,
offset=offset,
limit=limit, limit=limit,
name=name, name=name,
title=title, title=title,
@ -151,6 +155,8 @@ class ProjectGroupsController(rest.RestController):
response.headers['X-Total'] = str(group_count) response.headers['X-Total'] = str(group_count)
if marker_group: if marker_group:
response.headers['X-Marker'] = str(marker_group.id) response.headers['X-Marker'] = str(marker_group.id)
if offset is not None:
response.headers['X-Offset'] = str(offset)
return [wmodels.ProjectGroup.from_db_model(group) for group in groups] return [wmodels.ProjectGroup.from_db_model(group) for group in groups]

View File

@ -81,13 +81,15 @@ class ProjectsController(rest.RestController):
@decorators.db_exceptions @decorators.db_exceptions
@secure(checks.guest) @secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.Project], int, int, wtypes.text, wtypes.text, @wsme_pecan.wsexpose([wmodels.Project], int, int, int, wtypes.text,
int, wtypes.text, wtypes.text) wtypes.text, int, wtypes.text, wtypes.text)
def get(self, marker=None, limit=None, name=None, description=None, def get(self, marker=None, offset=None, limit=None, name=None,
project_group_id=None, sort_field='id', sort_dir='asc'): description=None, project_group_id=None, sort_field='id',
sort_dir='asc'):
"""Retrieve a list of projects. """Retrieve a list of projects.
:param marker: The resource id where the page should begin. :param marker: The resource id where the page should begin.
:param offset: The offset to start the page at.
:param limit: The number of projects to retrieve. :param limit: The number of projects to retrieve.
:param name: A string to filter the name by. :param name: A string to filter the name by.
:param description: A string to filter the description by. :param description: A string to filter the description by.
@ -102,10 +104,13 @@ class ProjectsController(rest.RestController):
limit = max(0, limit) limit = max(0, limit)
# Resolve the marker record. # Resolve the marker record.
marker_project = None
if marker is not None:
marker_project = projects_api.project_get(marker) marker_project = projects_api.project_get(marker)
projects = \ projects = \
projects_api.project_get_all(marker=marker_project, projects_api.project_get_all(marker=marker_project,
offset=offset,
limit=limit, limit=limit,
name=name, name=name,
description=description, description=description,
@ -123,6 +128,8 @@ class ProjectsController(rest.RestController):
response.headers['X-Total'] = str(project_count) response.headers['X-Total'] = str(project_count)
if marker_project: if marker_project:
response.headers['X-Marker'] = str(marker_project.id) response.headers['X-Marker'] = str(marker_project.id)
if offset is not None:
response.headers['X-Offset'] = str(offset)
return [wmodels.Project.from_db_model(p) for p in projects] return [wmodels.Project.from_db_model(p) for p in projects]

View File

@ -69,12 +69,12 @@ class StoriesController(rest.RestController):
@decorators.db_exceptions @decorators.db_exceptions
@secure(checks.guest) @secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.Story], wtypes.text, wtypes.text, @wsme_pecan.wsexpose([wmodels.Story], wtypes.text, wtypes.text,
[wtypes.text], int, int, int, [wtypes.text], int, int, [wtypes.text], int, int, int, [wtypes.text], int,
wtypes.text, wtypes.text, wtypes.text) int, int, wtypes.text, wtypes.text, wtypes.text)
def get_all(self, title=None, description=None, status=None, def get_all(self, title=None, description=None, status=None,
assignee_id=None, project_group_id=None, project_id=None, assignee_id=None, project_group_id=None, project_id=None,
tags=None, marker=None, limit=None, tags_filter_type='all', tags=None, marker=None, offset=None, limit=None,
sort_field='id', sort_dir='asc'): tags_filter_type='all', sort_field='id', sort_dir='asc'):
"""Retrieve definitions of all of the stories. """Retrieve definitions of all of the stories.
:param title: A string to filter the title by. :param title: A string to filter the title by.
@ -85,6 +85,7 @@ class StoriesController(rest.RestController):
:param project_id: Filter stories by project ID. :param project_id: Filter stories by project ID.
:param tags: A list of tags to filter by. :param tags: A list of tags to filter by.
:param marker: The resource id where the page should begin. :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 limit: The number of stories to retrieve.
:param tags_filter_type: Type of tags filter. :param tags_filter_type: Type of tags filter.
:param sort_field: The name of the field to sort on. :param sort_field: The name of the field to sort on.
@ -96,6 +97,8 @@ class StoriesController(rest.RestController):
limit = max(0, limit) limit = max(0, limit)
# Resolve the marker record. # Resolve the marker record.
marker_story = None
if marker:
marker_story = stories_api.story_get(marker) marker_story = stories_api.story_get(marker)
stories = stories_api \ stories = stories_api \
@ -107,6 +110,7 @@ class StoriesController(rest.RestController):
project_id=project_id, project_id=project_id,
tags=tags, tags=tags,
marker=marker_story, marker=marker_story,
offset=offset,
tags_filter_type=tags_filter_type, tags_filter_type=tags_filter_type,
limit=limit, sort_field=sort_field, limit=limit, sort_field=sort_field,
sort_dir=sort_dir) sort_dir=sort_dir)
@ -126,6 +130,8 @@ class StoriesController(rest.RestController):
response.headers['X-Total'] = str(story_count) response.headers['X-Total'] = str(story_count)
if marker_story: if marker_story:
response.headers['X-Marker'] = str(marker_story.id) response.headers['X-Marker'] = str(marker_story.id)
if offset is not None:
response.headers['X-Offset'] = str(offset)
return [wmodels.Story.from_db_model(s) for s in stories] return [wmodels.Story.from_db_model(s) for s in stories]

View File

@ -64,15 +64,16 @@ class TimeLineEventsController(rest.RestController):
@decorators.db_exceptions @decorators.db_exceptions
@secure(checks.guest) @secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.TimeLineEvent], int, [wtypes.text], int, int, @wsme_pecan.wsexpose([wmodels.TimeLineEvent], int, [wtypes.text], int,
wtypes.text, wtypes.text) int, int, wtypes.text, wtypes.text)
def get_all(self, story_id=None, event_type=None, marker=None, def get_all(self, story_id=None, event_type=None, marker=None,
limit=None, sort_field=None, sort_dir=None): offset=None, limit=None, sort_field=None, sort_dir=None):
"""Retrieve all events that have happened under specified story. """Retrieve all events that have happened under specified story.
:param story_id: Filter events by story ID. :param story_id: Filter events by story ID.
:param event_type: A selection of event types to get. :param event_type: A selection of event types to get.
:param marker: The resource id where the page should begin. :param marker: The resource id where the page should begin.
:param offset: The offset to start the page at.
:param limit: The number of events to retrieve. :param limit: The number of events to retrieve.
:param sort_field: The name of the field to sort on. :param sort_field: The name of the field to sort on.
:param sort_dir: Sort direction for results (asc, desc). :param sort_dir: Sort direction for results (asc, desc).
@ -92,6 +93,8 @@ class TimeLineEventsController(rest.RestController):
abort(400, msg) abort(400, msg)
# Resolve the marker record. # Resolve the marker record.
marker_event = None
if marker is not None:
marker_event = events_api.event_get(marker) marker_event = events_api.event_get(marker)
event_count = events_api.events_get_count(story_id=story_id, event_count = events_api.events_get_count(story_id=story_id,
@ -99,6 +102,7 @@ class TimeLineEventsController(rest.RestController):
events = events_api.events_get_all(story_id=story_id, events = events_api.events_get_all(story_id=story_id,
event_type=event_type, event_type=event_type,
marker=marker_event, marker=marker_event,
offset=offset,
limit=limit, limit=limit,
sort_field=sort_field, sort_field=sort_field,
sort_dir=sort_dir) sort_dir=sort_dir)
@ -109,6 +113,8 @@ class TimeLineEventsController(rest.RestController):
response.headers['X-Total'] = str(event_count) response.headers['X-Total'] = str(event_count)
if marker_event: if marker_event:
response.headers['X-Marker'] = str(marker_event.id) response.headers['X-Marker'] = str(marker_event.id)
if offset is not None:
response.headers['X-Offset'] = str(offset)
return [wmodels.TimeLineEvent.resolve_event_values( return [wmodels.TimeLineEvent.resolve_event_values(
wmodels.TimeLineEvent.from_db_model(event)) for event in events] wmodels.TimeLineEvent.from_db_model(event)) for event in events]

View File

@ -57,13 +57,14 @@ class UsersController(rest.RestController):
@decorators.db_exceptions @decorators.db_exceptions
@secure(checks.guest) @secure(checks.guest)
@wsme_pecan.wsexpose([wmodels.User], int, int, wtypes.text, wtypes.text, @wsme_pecan.wsexpose([wmodels.User], int, int, int, wtypes.text,
wtypes.text, wtypes.text) wtypes.text, wtypes.text, wtypes.text)
def get(self, marker=None, limit=None, full_name=None, def get(self, marker=None, offset=None, limit=None, full_name=None,
sort_field='id', sort_dir='asc'): sort_field='id', sort_dir='asc'):
"""Page and filter the users in storyboard. """Page and filter the users in storyboard.
:param marker: The resource id where the page should begin. :param marker: The resource id where the page should begin.
:param offset: The offset to start the page at.
:param limit: The number of users to retrieve. :param limit: The number of users to retrieve.
:param username: A string of characters to filter the username with. :param username: A string of characters to filter the username with.
:param full_name: A string of characters to filter the full_name with. :param full_name: A string of characters to filter the full_name with.
@ -76,9 +77,13 @@ class UsersController(rest.RestController):
limit = max(0, limit) limit = max(0, limit)
# Resolve the marker record. # Resolve the marker record.
marker_user = None
if marker is not None:
marker_user = users_api.user_get(marker) marker_user = users_api.user_get(marker)
users = users_api.user_get_all(marker=marker_user, limit=limit, users = users_api.user_get_all(marker=marker_user,
offset=offset,
limit=limit,
full_name=full_name, full_name=full_name,
filter_non_public=True, filter_non_public=True,
sort_field=sort_field, sort_field=sort_field,
@ -91,6 +96,8 @@ class UsersController(rest.RestController):
response.headers['X-Total'] = str(user_count) response.headers['X-Total'] = str(user_count)
if marker_user: if marker_user:
response.headers['X-Marker'] = str(marker_user.id) response.headers['X-Marker'] = str(marker_user.id)
if offset is not None:
response.headers['X-Offset'] = str(offset)
return [wmodels.User.from_db_model(u) for u in users] return [wmodels.User.from_db_model(u) for u in users]

View File

@ -95,15 +95,26 @@ def get_engine():
def paginate_query(query, model, limit, sort_key, marker=None, def paginate_query(query, model, limit, sort_key, marker=None,
sort_dir=None, sort_dirs=None): offset=None, sort_dir=None, sort_dirs=None):
if offset is not None:
# If we are doing offset-based pagination, don't set a
# limit or a marker.
# FIXME: Eventually the webclient should be smart enough
# to do marker-based pagination, at which point this will
# be unnecessary.
start, end = (offset, offset + limit)
limit, marker = (None, None)
try: try:
return utils_paginate_query(query=query, sorted_query = utils_paginate_query(query=query,
model=model, model=model,
limit=limit, limit=limit,
sort_keys=[sort_key], sort_keys=[sort_key],
marker=marker, marker=marker,
sort_dir=sort_dir, sort_dir=sort_dir,
sort_dirs=sort_dirs) sort_dirs=sort_dirs)
if offset is not None:
return sorted_query.slice(start, end)
return sorted_query
except ValueError as ve: except ValueError as ve:
raise exc.DBValueError(message=str(ve)) raise exc.DBValueError(message=str(ve))
except InvalidSortKey: except InvalidSortKey:
@ -187,8 +198,9 @@ def entity_get(kls, entity_id, filter_non_public=False, session=None):
return entity return entity
def entity_get_all(kls, filter_non_public=False, marker=None, limit=None, def entity_get_all(kls, filter_non_public=False, marker=None, offset=None,
sort_field='id', sort_dir='asc', session=None, **kwargs): limit=None, sort_field='id', sort_dir='asc', session=None,
**kwargs):
# Sanity checks, in case someone accidentally explicitly passes in 'None' # Sanity checks, in case someone accidentally explicitly passes in 'None'
if not sort_field: if not sort_field:
sort_field = 'id' sort_field = 'id'
@ -208,6 +220,7 @@ def entity_get_all(kls, filter_non_public=False, marker=None, limit=None,
limit=limit, limit=limit,
sort_key=sort_field, sort_key=sort_field,
marker=marker, marker=marker,
offset=offset,
sort_dir=sort_dir) sort_dir=sort_dir)
# Execute the query # Execute the query
@ -220,7 +233,7 @@ def entity_get_all(kls, filter_non_public=False, marker=None, limit=None,
raise exc.DBInvalidUnicodeParameter() raise exc.DBInvalidUnicodeParameter()
if len(entities) > 0 and filter_non_public: if len(entities) > 0 and filter_non_public:
sample_entity = entities[0] if len(entities) > 0 else None sample_entity = entities[0]
public_fields = getattr(sample_entity, "_public_fields", []) public_fields = getattr(sample_entity, "_public_fields", [])
entities = [_filter_non_public_fields(entity, public_fields) entities = [_filter_non_public_fields(entity, public_fields)

View File

@ -28,8 +28,8 @@ def project_get_by_name(name):
return query.filter_by(name=name).first() return query.filter_by(name=name).first()
def project_get_all(marker=None, limit=None, sort_field=None, sort_dir=None, def project_get_all(marker=None, offset=None, limit=None, sort_field=None,
project_group_id=None, **kwargs): sort_dir=None, project_group_id=None, **kwargs):
# Sanity checks, in case someone accidentally explicitly passes in 'None' # Sanity checks, in case someone accidentally explicitly passes in 'None'
if not sort_field: if not sort_field:
sort_field = 'id' sort_field = 'id'
@ -45,6 +45,7 @@ def project_get_all(marker=None, limit=None, sort_field=None, sort_dir=None,
limit=limit, limit=limit,
sort_key=sort_field, sort_key=sort_field,
marker=marker, marker=marker,
offset=offset,
sort_dir=sort_dir) sort_dir=sort_dir)
# Execute the query # Execute the query

View File

@ -56,8 +56,8 @@ def story_get(story_id, session=None):
def story_get_all(title=None, description=None, status=None, assignee_id=None, def story_get_all(title=None, description=None, status=None, assignee_id=None,
project_group_id=None, project_id=None, tags=None, project_group_id=None, project_id=None, tags=None,
marker=None, limit=None, tags_filter_type="all", marker=None, offset=None, limit=None,
sort_field='id', sort_dir='asc'): tags_filter_type="all", sort_field='id', sort_dir='asc'):
# Sanity checks, in case someone accidentally explicitly passes in 'None' # Sanity checks, in case someone accidentally explicitly passes in 'None'
if not sort_field: if not sort_field:
sort_field = 'id' sort_field = 'id'
@ -86,12 +86,12 @@ def story_get_all(title=None, description=None, status=None, assignee_id=None,
query = query.filter(models.StorySummary.status.in_(status)) query = query.filter(models.StorySummary.status.in_(status))
# paginate the query # paginate the query
query = api_base.paginate_query(query=query, query = api_base.paginate_query(query=query,
model=models.StorySummary, model=models.StorySummary,
limit=limit, limit=limit,
sort_key=sort_field, sort_key=sort_field,
marker=marker, marker=marker,
offset=offset,
sort_dir=sort_dir) sort_dir=sort_dir)
raw_stories = query.all() raw_stories = query.all()

View File

@ -25,10 +25,12 @@ def user_get(user_id, filter_non_public=False):
return entity return entity
def user_get_all(marker=None, limit=None, filter_non_public=False, def user_get_all(marker=None, offset=None, limit=None,
sort_field=None, sort_dir=None, **kwargs): filter_non_public=False, sort_field=None, sort_dir=None,
**kwargs):
return api_base.entity_get_all(models.User, return api_base.entity_get_all(models.User,
marker=marker, marker=marker,
offset=offset,
limit=limit, limit=limit,
filter_non_public=filter_non_public, filter_non_public=filter_non_public,
sort_field=sort_field, sort_field=sort_field,