server/users: support wildcards in user search
This commit is contained in:
parent
286df9faf3
commit
35c549493c
4
API.md
4
API.md
|
@ -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:
|
||||||
|
|
|
@ -70,6 +70,8 @@ take following form:</p>
|
||||||
<li><code><year>-<month>-<day></code></li>
|
<li><code><year>-<month>-<day></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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
Loading…
Reference in New Issue