server/users: support wildcards in user search

This commit is contained in:
rr- 2016-04-14 20:16:05 +02:00
parent 286df9faf3
commit 35c549493c
5 changed files with 56 additions and 34 deletions

4
API.md
View File

@ -117,7 +117,7 @@ data.
| `<value>` | Description | | `<value>` | Description |
| ----------------- | ------------------------------------------------ | | ----------------- | ------------------------------------------------ |
| `name` | having given name (doesn't accept wildcards yet) | | `name` | having given name (accepts wildcards) |
| `creation-date` | registered at given date | | `creation-date` | registered at given date |
| `creation-time` | alias of `creation-date` | | `creation-time` | alias of `creation-date` |
| `last-login-date` | whose most recent login date matches given date | | `last-login-date` | whose most recent login date matches given date |
@ -390,6 +390,8 @@ Date/time values can be of following form:
- `<year>-<month>` - `<year>-<month>`
- `<year>-<month>-<day>` - `<year>-<month>-<day>`
Some fields, such as user names, can take wildcards (`*`).
**Example** **Example**
Searching for posts with following query: Searching for posts with following query:

View File

@ -70,6 +70,8 @@ take following form:</p>
<li><code>&lt;year&gt;-&lt;month&gt;-&lt;day&gt;</code></li> <li><code>&lt;year&gt;-&lt;month&gt;-&lt;day&gt;</code></li>
</ul> </ul>
<p>Some fields, such as user names, can take wildcards (<code>*</code>).</p>
<p>All tokens can be negated by prepending them with <code>-</code>.</p> <p>All tokens can be negated by prepending them with <code>-</code>.</p>
<p>Order token values can be appended with <code>,asc</code> or <p>Order token values can be appended with <code>,asc</code> or

View File

@ -3,32 +3,19 @@ import szurubooru.errors
from szurubooru.util import misc from szurubooru.util import misc
from szurubooru.search import criteria from szurubooru.search import criteria
def _apply_criterion_to_column( def _apply_num_criterion_to_column(column, query, criterion):
column, query, criterion, allow_composite=True, allow_ranged=True):
''' Decorate SQLAlchemy filter on given column using supplied criterion. ''' ''' Decorate SQLAlchemy filter on given column using supplied criterion. '''
if isinstance(criterion, criteria.StringSearchCriterion): if isinstance(criterion, criteria.StringSearchCriterion):
expr = column == criterion.value expr = column == criterion.value
if criterion.negated:
expr = ~expr
return query.filter(expr)
elif isinstance(criterion, criteria.ArraySearchCriterion): elif isinstance(criterion, criteria.ArraySearchCriterion):
if not allow_composite:
raise szurubooru.errors.SearchError(
'Composite token %r is invalid in this context.' % (criterion,))
expr = column.in_(criterion.values) expr = column.in_(criterion.values)
if criterion.negated:
expr = ~expr
return query.filter(expr)
elif isinstance(criterion, criteria.RangedSearchCriterion): elif isinstance(criterion, criteria.RangedSearchCriterion):
if not allow_ranged:
raise szurubooru.errors.SearchError(
'Ranged token %r is invalid in this context.' % (criterion,))
expr = column.between(criterion.min_value, criterion.max_value) expr = column.between(criterion.min_value, criterion.max_value)
else:
assert False
if criterion.negated: if criterion.negated:
expr = ~expr expr = ~expr
return query.filter(expr) return query.filter(expr)
else:
raise RuntimeError('Invalid search type: %r.' % (criterion,))
def _apply_date_criterion_to_column(column, query, criterion): def _apply_date_criterion_to_column(column, query, criterion):
''' '''
@ -38,17 +25,11 @@ def _apply_date_criterion_to_column(column, query, criterion):
if isinstance(criterion, criteria.StringSearchCriterion): if isinstance(criterion, criteria.StringSearchCriterion):
min_date, max_date = misc.parse_time_range(criterion.value) min_date, max_date = misc.parse_time_range(criterion.value)
expr = column.between(min_date, max_date) expr = column.between(min_date, max_date)
if criterion.negated:
expr = ~expr
return query.filter(expr)
elif isinstance(criterion, criteria.ArraySearchCriterion): elif isinstance(criterion, criteria.ArraySearchCriterion):
expr = sqlalchemy.sql.false() expr = sqlalchemy.sql.false()
for value in criterion.values: for value in criterion.values:
min_date, max_date = misc.parse_time_range(value) min_date, max_date = misc.parse_time_range(value)
expr = expr | column.between(min_date, max_date) expr = expr | column.between(min_date, max_date)
if criterion.negated:
expr = ~expr
return query.filter(expr)
elif isinstance(criterion, criteria.RangedSearchCriterion): elif isinstance(criterion, criteria.RangedSearchCriterion):
assert criterion.min_value or criterion.max_value assert criterion.min_value or criterion.max_value
if criterion.min_value and criterion.max_value: if criterion.min_value and criterion.max_value:
@ -61,6 +42,28 @@ def _apply_date_criterion_to_column(column, query, criterion):
elif criterion.max_value: elif criterion.max_value:
max_date = misc.parse_time_range(criterion.max_value)[1] max_date = misc.parse_time_range(criterion.max_value)[1]
expr = column <= max_date expr = column <= max_date
else:
assert False
if criterion.negated:
expr = ~expr
return query.filter(expr)
def _apply_str_criterion_to_column(column, query, criterion):
'''
Decorate SQLAlchemy filter on given column using supplied criterion.
Parse potential wildcards inside the criterion.
'''
if isinstance(criterion, criteria.StringSearchCriterion):
expr = column.like(criterion.value.replace('*', '%'))
elif isinstance(criterion, criteria.ArraySearchCriterion):
expr = sqlalchemy.sql.false()
for value in criterion.values:
expr = expr | column.like(value.replace('*', '%'))
elif isinstance(criterion, criteria.RangedSearchCriterion):
raise szurubooru.errors.SearchError(
'Composite token %r is invalid in this context.' % (criterion,))
else:
assert False
if criterion.negated: if criterion.negated:
expr = ~expr expr = ~expr
return query.filter(expr) return query.filter(expr)
@ -85,11 +88,14 @@ class BaseSearchConfig(object):
def order_columns(self): def order_columns(self):
raise NotImplementedError() raise NotImplementedError()
def _create_basic_filter( def _create_num_filter(self, column):
self, column, allow_composite=True, allow_ranged=True): return lambda query, criterion: _apply_num_criterion_to_column(
return lambda query, criterion: _apply_criterion_to_column( column, query, criterion)
column, query, criterion, allow_composite, allow_ranged)
def _create_date_filter(self, column): def _create_date_filter(self, column):
return lambda query, criterion: _apply_date_criterion_to_column( return lambda query, criterion: _apply_date_criterion_to_column(
column, query, criterion) column, query, criterion)
def _create_str_filter(self, column):
return lambda query, criterion: _apply_str_criterion_to_column(
column, query, criterion)

View File

@ -13,7 +13,7 @@ class UserSearchConfig(BaseSearchConfig):
@property @property
def anonymous_filter(self): def anonymous_filter(self):
return self._create_basic_filter(db.User.name, allow_ranged=False) return self._create_str_filter(db.User.name)
@property @property
def special_filters(self): def special_filters(self):
@ -22,7 +22,7 @@ class UserSearchConfig(BaseSearchConfig):
@property @property
def named_filters(self): def named_filters(self):
return { return {
'name': self._create_basic_filter(db.User.name, allow_ranged=False), 'name': self._create_str_filter(db.User.name),
'creation-date': self._create_date_filter(db.User.creation_time), 'creation-date': self._create_date_filter(db.User.creation_time),
'creation-time': self._create_date_filter(db.User.creation_time), 'creation-time': self._create_date_filter(db.User.creation_time),
'last-login-date': self._create_date_filter(db.User.last_login_time), 'last-login-date': self._create_date_filter(db.User.last_login_time),

View File

@ -96,6 +96,18 @@ class TestUserSearchExecutor(DatabaseTestCase):
self._test('name:u1', 1, 100, 1, ['u1']) self._test('name:u1', 1, 100, 1, ['u1'])
self._test('name:u2', 1, 100, 1, ['u2']) self._test('name:u2', 1, 100, 1, ['u2'])
def test_filter_by_name_wildcards(self):
self.session.add(util.mock_user('user1'))
self.session.add(util.mock_user('user2'))
self._test('name:*1', 1, 100, 1, ['user1'])
self._test('name:*2', 1, 100, 1, ['user2'])
self._test('name:*', 1, 100, 2, ['user1', 'user2'])
self._test('name:u*', 1, 100, 2, ['user1', 'user2'])
self._test('name:*ser*', 1, 100, 2, ['user1', 'user2'])
self._test('name:*zer*', 1, 100, 0, [])
self._test('name:zer*', 1, 100, 0, [])
self._test('name:*zer', 1, 100, 0, [])
def test_filter_by_negated_name(self): def test_filter_by_negated_name(self):
self.session.add(util.mock_user('u1')) self.session.add(util.mock_user('u1'))
self.session.add(util.mock_user('u2')) self.session.add(util.mock_user('u2'))