diff --git a/API.md b/API.md index ecc9d06..0744c91 100644 --- a/API.md +++ b/API.md @@ -117,7 +117,7 @@ data. | `` | Description | | ----------------- | ------------------------------------------------ | - | `name` | having given name (doesn't accept wildcards yet) | + | `name` | having given name (accepts wildcards) | | `creation-date` | registered at given date | | `creation-time` | alias of `creation-date` | | `last-login-date` | whose most recent login date matches given date | @@ -390,6 +390,8 @@ Date/time values can be of following form: - `-` - `--` +Some fields, such as user names, can take wildcards (`*`). + **Example** Searching for posts with following query: diff --git a/client/html/help-search.hbs b/client/html/help-search.hbs index 7586326..6c60730 100644 --- a/client/html/help-search.hbs +++ b/client/html/help-search.hbs @@ -70,6 +70,8 @@ take following form:

  • <year>-<month>-<day>
  • +

    Some fields, such as user names, can take wildcards (*).

    +

    All tokens can be negated by prepending them with -.

    Order token values can be appended with ,asc or diff --git a/server/szurubooru/search/base_search_config.py b/server/szurubooru/search/base_search_config.py index 0e6712f..8de0a23 100644 --- a/server/szurubooru/search/base_search_config.py +++ b/server/szurubooru/search/base_search_config.py @@ -3,32 +3,19 @@ import szurubooru.errors from szurubooru.util import misc from szurubooru.search import criteria -def _apply_criterion_to_column( - column, query, criterion, allow_composite=True, allow_ranged=True): +def _apply_num_criterion_to_column(column, query, criterion): ''' Decorate SQLAlchemy filter on given column using supplied criterion. ''' if isinstance(criterion, criteria.StringSearchCriterion): expr = column == criterion.value - if criterion.negated: - expr = ~expr - return query.filter(expr) 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) - if criterion.negated: - expr = ~expr - return query.filter(expr) 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) - if criterion.negated: - expr = ~expr - return query.filter(expr) else: - raise RuntimeError('Invalid search type: %r.' % (criterion,)) + assert False + if criterion.negated: + expr = ~expr + return query.filter(expr) 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): min_date, max_date = misc.parse_time_range(criterion.value) expr = column.between(min_date, max_date) - if criterion.negated: - expr = ~expr - return query.filter(expr) elif isinstance(criterion, criteria.ArraySearchCriterion): expr = sqlalchemy.sql.false() for value in criterion.values: min_date, max_date = misc.parse_time_range(value) expr = expr | column.between(min_date, max_date) - if criterion.negated: - expr = ~expr - return query.filter(expr) elif isinstance(criterion, criteria.RangedSearchCriterion): assert criterion.min_value or criterion.max_value if criterion.min_value and criterion.max_value: @@ -61,9 +42,31 @@ def _apply_date_criterion_to_column(column, query, criterion): elif criterion.max_value: max_date = misc.parse_time_range(criterion.max_value)[1] expr = column <= max_date - if criterion.negated: - expr = ~expr - return query.filter(expr) + 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: + expr = ~expr + return query.filter(expr) class BaseSearchConfig(object): def create_query(self, session): @@ -85,11 +88,14 @@ class BaseSearchConfig(object): def order_columns(self): raise NotImplementedError() - def _create_basic_filter( - self, column, allow_composite=True, allow_ranged=True): - return lambda query, criterion: _apply_criterion_to_column( - column, query, criterion, allow_composite, allow_ranged) + def _create_num_filter(self, column): + return lambda query, criterion: _apply_num_criterion_to_column( + column, query, criterion) def _create_date_filter(self, column): return lambda query, criterion: _apply_date_criterion_to_column( column, query, criterion) + + def _create_str_filter(self, column): + return lambda query, criterion: _apply_str_criterion_to_column( + column, query, criterion) diff --git a/server/szurubooru/search/user_search_config.py b/server/szurubooru/search/user_search_config.py index 589e1d5..45796f1 100644 --- a/server/szurubooru/search/user_search_config.py +++ b/server/szurubooru/search/user_search_config.py @@ -13,7 +13,7 @@ class UserSearchConfig(BaseSearchConfig): @property def anonymous_filter(self): - return self._create_basic_filter(db.User.name, allow_ranged=False) + return self._create_str_filter(db.User.name) @property def special_filters(self): @@ -22,7 +22,7 @@ class UserSearchConfig(BaseSearchConfig): @property def named_filters(self): 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-time': self._create_date_filter(db.User.creation_time), 'last-login-date': self._create_date_filter(db.User.last_login_time), diff --git a/server/szurubooru/tests/search/test_user_search_config.py b/server/szurubooru/tests/search/test_user_search_config.py index 0cc6c1d..c151cd5 100644 --- a/server/szurubooru/tests/search/test_user_search_config.py +++ b/server/szurubooru/tests/search/test_user_search_config.py @@ -96,6 +96,18 @@ class TestUserSearchExecutor(DatabaseTestCase): self._test('name:u1', 1, 100, 1, ['u1']) 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): self.session.add(util.mock_user('u1')) self.session.add(util.mock_user('u2'))