server/api: let users control page size

This commit is contained in:
rr- 2016-04-10 22:13:38 +02:00
parent fba239855c
commit d97a332d23
5 changed files with 64 additions and 50 deletions

2
API.md
View file

@ -77,7 +77,7 @@ data.
### Listing users
Request: `GET /users/?page=<page>&query=<query>`
Request: `GET /users/?page=<page>&pageSize=<page-size>&query=<query>`
Output:
```json5
{

View file

@ -38,11 +38,13 @@ class UserListApi(BaseApi):
auth.verify_privilege(context.user, 'users:list')
query = context.get_param_as_string('query')
page = context.get_param_as_int('page', 1)
count, user_list = self._search_executor.execute(context.session, query, page)
page_size = min(100, context.get_param_as_int('pageSize', required=False) or 100)
count, user_list = self._search_executor.execute(
context.session, query, page, page_size)
return {
'query': query,
'page': page,
'pageSize': self._search_executor.page_size,
'pageSize': page_size,
'total': count,
'users': [_serialize_user(context.user, user) for user in user_list],
}

View file

@ -13,17 +13,16 @@ class SearchExecutor(object):
ORDER_ASC = 2
def __init__(self, search_config):
self.page_size = 100
self._search_config = search_config
def execute(self, session, query_text, page):
def execute(self, session, query_text, page, page_size):
'''
Parse input and return tuple containing total record count and filtered
entities.
'''
filter_query = self._prepare(session, query_text)
entities = filter_query \
.offset((page - 1) * self.page_size).limit(self.page_size).all()
.offset((page - 1) * page_size).limit(page_size).all()
count_query = filter_query.statement \
.with_only_columns([sqlalchemy.func.count()]).order_by(None)
count = filter_query.session.execute(count_query).scalar()

View file

@ -25,12 +25,16 @@ def mock_context(parent):
parent.context = context
def mock_params(context, params):
def get_param_as_string(key, default=None):
def get_param_as_string(key, default=None, required=False):
if key not in params:
if required:
raise RuntimeError('Param is missing!')
return default
return params[key]
def get_param_as_int(key, default=None):
def get_param_as_int(key, default=None, required=False):
if key not in params:
if required:
raise RuntimeError('Param is missing!')
return default
return int(params[key])
context.get_param_as_string = get_param_as_string

View file

@ -9,11 +9,20 @@ class TestUserSearchExecutor(DatabaseTestCase):
self.search_config = search.UserSearchConfig()
self.executor = search.SearchExecutor(self.search_config)
def _test(self, query, page, expected_count, expected_user_names):
count, users = self.executor.execute(self.session, query, page)
def _test(self, query, page, page_size, expected_count, expected_user_names):
count, users = self.executor.execute(self.session, query, page, page_size)
self.assertEqual(count, expected_count)
self.assertEqual([u.name for u in users], expected_user_names)
def _test_raises(self, query, page, page_size):
self.assertRaises(
errors.SearchError,
self.executor.execute,
self.session,
query,
page,
page_size)
def test_filter_by_creation_time(self):
user1 = util.mock_user('u1')
user2 = util.mock_user('u2')
@ -21,7 +30,7 @@ class TestUserSearchExecutor(DatabaseTestCase):
user2.creation_time = datetime(2015, 1, 1)
self.session.add_all([user1, user2])
for alias in ['creation-time', 'creation-date']:
self._test('%s:2014' % alias, 1, 1, ['u1'])
self._test('%s:2014' % alias, 1, 100, 1, ['u1'])
def test_filter_by_negated_creation_time(self):
user1 = util.mock_user('u1')
@ -30,7 +39,7 @@ class TestUserSearchExecutor(DatabaseTestCase):
user2.creation_time = datetime(2015, 1, 1)
self.session.add_all([user1, user2])
for alias in ['creation-time', 'creation-date']:
self._test('-%s:2014' % alias, 1, 1, ['u2'])
self._test('-%s:2014' % alias, 1, 100, 1, ['u2'])
def test_filter_by_ranged_creation_time(self):
user1 = util.mock_user('u1')
@ -41,12 +50,11 @@ class TestUserSearchExecutor(DatabaseTestCase):
user3.creation_time = datetime(2015, 1, 1)
self.session.add_all([user1, user2, user3])
for alias in ['creation-time', 'creation-date']:
self._test('%s:2014..2014-06' % alias, 1, 2, ['u1', 'u2'])
self._test('%s:2014-06..2015-01-01' % alias, 1, 2, ['u2', 'u3'])
self._test('%s:2014-06..' % alias, 1, 2, ['u2', 'u3'])
self._test('%s:..2014-06' % alias, 1, 2, ['u1', 'u2'])
self.assertRaises(
errors.SearchError, self.executor.execute, self.session, '%s:..', 1)
self._test('%s:2014..2014-06' % alias, 1, 100, 2, ['u1', 'u2'])
self._test('%s:2014-06..2015-01-01' % alias, 1, 100, 2, ['u2', 'u3'])
self._test('%s:2014-06..' % alias, 1, 100, 2, ['u2', 'u3'])
self._test('%s:..2014-06' % alias, 1, 100, 2, ['u1', 'u2'])
self._test_raises('%s:..' % alias, 1, 100)
def test_filter_by_negated_ranged_creation_time(self):
user1 = util.mock_user('u1')
@ -57,8 +65,8 @@ class TestUserSearchExecutor(DatabaseTestCase):
user3.creation_time = datetime(2015, 1, 1)
self.session.add_all([user1, user2, user3])
for alias in ['creation-time', 'creation-date']:
self._test('-%s:2014..2014-06' % alias, 1, 1, ['u3'])
self._test('-%s:2014-06..2015-01-01' % alias, 1, 1, ['u1'])
self._test('-%s:2014..2014-06' % alias, 1, 100, 1, ['u3'])
self._test('-%s:2014-06..2015-01-01' % alias, 1, 100, 1, ['u1'])
def test_filter_by_composite_creation_time(self):
user1 = util.mock_user('u1')
@ -69,7 +77,7 @@ class TestUserSearchExecutor(DatabaseTestCase):
user3.creation_time = datetime(2015, 1, 1)
self.session.add_all([user1, user2, user3])
for alias in ['creation-time', 'creation-date']:
self._test('%s:2014-01,2015' % alias, 1, 2, ['u1', 'u3'])
self._test('%s:2014-01,2015' % alias, 1, 100, 2, ['u1', 'u3'])
def test_filter_by_negated_composite_creation_time(self):
user1 = util.mock_user('u1')
@ -80,52 +88,50 @@ class TestUserSearchExecutor(DatabaseTestCase):
user3.creation_time = datetime(2015, 1, 1)
self.session.add_all([user1, user2, user3])
for alias in ['creation-time', 'creation-date']:
self._test('-%s:2014-01,2015' % alias, 1, 1, ['u2'])
self._test('-%s:2014-01,2015' % alias, 1, 100, 1, ['u2'])
def test_filter_by_name(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self._test('name:u1', 1, 1, ['u1'])
self._test('name:u2', 1, 1, ['u2'])
self._test('name:u1', 1, 100, 1, ['u1'])
self._test('name:u2', 1, 100, 1, ['u2'])
def test_filter_by_negated_name(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self._test('-name:u1', 1, 1, ['u2'])
self._test('-name:u2', 1, 1, ['u1'])
self._test('-name:u1', 1, 100, 1, ['u2'])
self._test('-name:u2', 1, 100, 1, ['u1'])
def test_filter_by_composite_name(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self.session.add(util.mock_user('u3'))
self._test('name:u1,u2', 1, 2, ['u1', 'u2'])
self._test('name:u1,u2', 1, 100, 2, ['u1', 'u2'])
def test_filter_by_negated_composite_name(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self.session.add(util.mock_user('u3'))
self._test('-name:u1,u3', 1, 1, ['u2'])
self._test('-name:u1,u3', 1, 100, 1, ['u2'])
def test_filter_by_ranged_name(self):
self.assertRaises(
errors.SearchError, self.executor.execute, self.session, 'name:u1..u2', 1)
self._test_raises('name:u1..u2', 1, 100)
def test_paging(self):
self.executor.page_size = 1
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self._test('', 1, 2, ['u1'])
self._test('', 2, 2, ['u2'])
self._test('', 1, 1, 2, ['u1'])
self._test('', 2, 1, 2, ['u2'])
def test_order_by_name(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self._test('order:name', 1, 2, ['u1', 'u2'])
self._test('-order:name', 1, 2, ['u2', 'u1'])
self._test('order:name,asc', 1, 2, ['u1', 'u2'])
self._test('order:name,desc', 1, 2, ['u2', 'u1'])
self._test('-order:name,asc', 1, 2, ['u2', 'u1'])
self._test('-order:name,desc', 1, 2, ['u1', 'u2'])
self._test('order:name', 1, 100, 2, ['u1', 'u2'])
self._test('-order:name', 1, 100, 2, ['u2', 'u1'])
self._test('order:name,asc', 1, 100, 2, ['u1', 'u2'])
self._test('order:name,desc', 1, 100, 2, ['u2', 'u1'])
self._test('-order:name,asc', 1, 100, 2, ['u2', 'u1'])
self._test('-order:name,desc', 1, 100, 2, ['u1', 'u2'])
def test_invalid_tokens(self):
for query in [
@ -135,20 +141,24 @@ class TestUserSearchExecutor(DatabaseTestCase):
'order:name,asc,desc',
'bad:x',
'special:unsupported']:
self.assertRaises(
errors.SearchError, self.executor.execute, self.session, query, 1)
self._test_raises(query, 1, 100)
def test_anonymous(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self._test('u1', 1, 1, ['u1'])
self._test('u2', 1, 1, ['u2'])
self._test('u1', 1, 100, 1, ['u1'])
self._test('u2', 1, 100, 1, ['u2'])
def test_empty_search(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self._test('', 1, 100, 2, ['u1', 'u2'])
def test_negated_anonymous(self):
self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2'))
self._test('-u1', 1, 1, ['u2'])
self._test('-u2', 1, 1, ['u1'])
self._test('-u1', 1, 100, 1, ['u2'])
self._test('-u2', 1, 100, 1, ['u1'])
def test_combining(self):
user1 = util.mock_user('u1')
@ -158,10 +168,9 @@ class TestUserSearchExecutor(DatabaseTestCase):
user2.creation_time = datetime(2014, 6, 1)
user3.creation_time = datetime(2015, 1, 1)
self.session.add_all([user1, user2, user3])
self._test('creation-time:2014 u1', 1, 1, ['u1'])
self._test('creation-time:2014 u2', 1, 1, ['u2'])
self._test('creation-time:2016 u2', 1, 0, [])
self._test('creation-time:2014 u1', 1, 100, 1, ['u1'])
self._test('creation-time:2014 u2', 1, 100, 1, ['u2'])
self._test('creation-time:2016 u2', 1, 100, 0, [])
def test_special(self):
self.assertRaises(
errors.SearchError, self.executor.execute, self.session, 'special:-', 1)
self._test_raises('special:-', 1, 100)