From 61d2fb88ea7430b5d005b14958c940ab90db68cc Mon Sep 17 00:00:00 2001
From: rr-
Special tokens
None.
+ +Anonymous tokens
+ +Same as name
token.
Named tokens
+ +name |
+ having given name (accepts wildcards) | +
category |
+ having given category | +
creation-date |
+ created at given date | +
creation-time |
+ alias of creation-date |
+
last-edit-date |
+ edited at given date | +
last-edit-time |
+ alias of last-edit-date |
+
edit-date |
+ alias of last-edit-date |
+
edit-time |
+ alias of last-edit-date |
+
usages |
+ used in given number of posts | +
usage-count |
+ alias of usages |
+
post-count |
+ alias of usages |
+
suggestion-count |
+ with given number of suggestions | +
implication-count |
+ with given number of implications | +
Order tokens
+ +random |
+ as random as it can get | +
name |
+ A to Z | +
category |
+ category (A to Z) | +
creation-date |
+ recently created first | +
creation-time |
+ alias of creation-date |
+
last-edit-date |
+ recently edited first | +
last-edit-time |
+ alias of creation-time |
+
edit-date |
+ alias of creation-time |
+
edit-time |
+ alias of creation-time |
+
usages |
+ used in most posts first | +
usage-count |
+ alias of usages |
+
post-count |
+ alias of usages |
+
suggestion-count |
+ with most suggestions first | +
implication-count |
+ with most implications first | +
Special tokens
+ +None.
diff --git a/server/szurubooru/api/tag_api.py b/server/szurubooru/api/tag_api.py index ac1b471..c65f97d 100644 --- a/server/szurubooru/api/tag_api.py +++ b/server/szurubooru/api/tag_api.py @@ -1,4 +1,5 @@ import datetime +from szurubooru import search from szurubooru.util import auth, tags from szurubooru.api.base_api import BaseApi @@ -15,8 +16,25 @@ def _serialize_tag(tag): } class TagListApi(BaseApi): + def __init__(self): + super().__init__() + self._search_executor = search.SearchExecutor(search.TagSearchConfig()) + def get(self, ctx): - raise NotImplementedError() + auth.verify_privilege(ctx.user, 'tags:list') + query = ctx.get_param_as_string('query') + page = ctx.get_param_as_int('page', default=1, min=1) + page_size = ctx.get_param_as_int( + 'pageSize', default=100, min=1, max=100) + count, tag_list = self._search_executor.execute( + ctx.session, query, page, page_size) + return { + 'query': query, + 'page': page, + 'pageSize': page_size, + 'total': count, + 'tags': [_serialize_tag(tag) for tag in tag_list], + } def post(self, ctx): auth.verify_privilege(ctx.user, 'tags:create') diff --git a/server/szurubooru/search/__init__.py b/server/szurubooru/search/__init__.py index f614db6..1f913cc 100644 --- a/server/szurubooru/search/__init__.py +++ b/server/szurubooru/search/__init__.py @@ -2,3 +2,4 @@ from szurubooru.search.search_executor import SearchExecutor from szurubooru.search.user_search_config import UserSearchConfig +from szurubooru.search.tag_search_config import TagSearchConfig diff --git a/server/szurubooru/search/base_search_config.py b/server/szurubooru/search/base_search_config.py index 1d63830..df29b4f 100644 --- a/server/szurubooru/search/base_search_config.py +++ b/server/szurubooru/search/base_search_config.py @@ -3,74 +3,9 @@ import szurubooru.errors from szurubooru.util import misc from szurubooru.search import criteria -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 - elif isinstance(criterion, criteria.ArraySearchCriterion): - expr = column.in_(criterion.values) - elif isinstance(criterion, criteria.RangedSearchCriterion): - expr = column.between(criterion.min_value, criterion.max_value) - else: - assert False - if criterion.negated: - expr = ~expr - return query.filter(expr) - -def _apply_date_criterion_to_column(column, query, criterion): - ''' - Decorate SQLAlchemy filter on given column using supplied criterion. - Parse the datetime inside the criterion. - ''' - if isinstance(criterion, criteria.StringSearchCriterion): - min_date, max_date = misc.parse_time_range(criterion.value) - expr = column.between(min_date, max_date) - 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) - elif isinstance(criterion, criteria.RangedSearchCriterion): - assert criterion.min_value or criterion.max_value - if criterion.min_value and criterion.max_value: - min_date = misc.parse_time_range(criterion.min_value)[0] - max_date = misc.parse_time_range(criterion.max_value)[1] - expr = column.between(min_date, max_date) - elif criterion.min_value: - min_date = misc.parse_time_range(criterion.min_value)[0] - expr = column >= min_date - elif criterion.max_value: - max_date = misc.parse_time_range(criterion.max_value)[1] - 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: - expr = ~expr - return query.filter(expr) - class BaseSearchConfig(object): - ORDER_DESC = 1 - ORDER_ASC = 2 + ORDER_DESC = 0 + ORDER_ASC = 1 def create_query(self, session): raise NotImplementedError() @@ -91,14 +26,88 @@ class BaseSearchConfig(object): def order_columns(self): raise NotImplementedError() - def _create_num_filter(self, column): - return lambda query, criterion: _apply_num_criterion_to_column( - column, query, criterion) + @staticmethod + def _apply_num_criterion_to_column(column, criterion): + ''' + Decorate SQLAlchemy filter on given column using supplied criterion. + ''' + if isinstance(criterion, criteria.StringSearchCriterion): + expr = column == int(criterion.value) + elif isinstance(criterion, criteria.ArraySearchCriterion): + expr = column.in_(int(value) for value in criterion.values) + elif isinstance(criterion, criteria.RangedSearchCriterion): + expr = column.between( + int(criterion.min_value), int(criterion.max_value)) + else: + assert False + if criterion.negated: + expr = ~expr + return expr - def _create_date_filter(self, column): - return lambda query, criterion: _apply_date_criterion_to_column( - column, query, criterion) + @staticmethod + def _create_num_filter(column): + return lambda query, criterion: query.filter( + BaseSearchConfig._apply_num_criterion_to_column(column, criterion)) - def _create_str_filter(self, column): - return lambda query, criterion: _apply_str_criterion_to_column( - column, query, criterion) + @staticmethod + def _apply_str_criterion_to_column(column, 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 expr + + @staticmethod + def _create_str_filter(column): + return lambda query, criterion: query.filter( + BaseSearchConfig._apply_str_criterion_to_column(column, criterion)) + + @staticmethod + def _apply_date_criterion_to_column(column, criterion): + ''' + Decorate SQLAlchemy filter on given column using supplied criterion. + Parse the datetime inside the criterion. + ''' + if isinstance(criterion, criteria.StringSearchCriterion): + min_date, max_date = misc.parse_time_range(criterion.value) + expr = column.between(min_date, max_date) + 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) + elif isinstance(criterion, criteria.RangedSearchCriterion): + assert criterion.min_value or criterion.max_value + if criterion.min_value and criterion.max_value: + min_date = misc.parse_time_range(criterion.min_value)[0] + max_date = misc.parse_time_range(criterion.max_value)[1] + expr = column.between(min_date, max_date) + elif criterion.min_value: + min_date = misc.parse_time_range(criterion.min_value)[0] + expr = column >= min_date + elif criterion.max_value: + max_date = misc.parse_time_range(criterion.max_value)[1] + expr = column <= max_date + else: + assert False + if criterion.negated: + expr = ~expr + return expr + + @staticmethod + def _create_date_filter(column): + return lambda query, criterion: query.filter( + BaseSearchConfig._apply_date_criterion_to_column(column, criterion)) diff --git a/server/szurubooru/search/search_executor.py b/server/szurubooru/search/search_executor.py index 154d5f2..203752d 100644 --- a/server/szurubooru/search/search_executor.py +++ b/server/szurubooru/search/search_executor.py @@ -48,28 +48,7 @@ class SearchExecutor(object): def _handle_key_value(self, query, key, value, negated): if key == 'order': - if value.count(',') == 0: - order = None - elif value.count(',') == 1: - value, order_str = value.split(',') - if order_str == 'asc': - order = self._search_config.ORDER_ASC - elif order_str == 'desc': - order = self._search_config.ORDER_DESC - else: - raise errors.SearchError( - 'Unknown search direction: %r.' % order_str) - else: - raise errors.SearchError( - 'Too many commas in order search token.') - if negated: - if order == self._search_config.ORDER_DESC: - order = self._search_config.ORDER_ASC - elif order == self._search_config.ORDER_ASC: - order = self._search_config.ORDER_DESC - else: - order = -1 - return self._handle_order(query, value, order) + return self._handle_order(query, value, negated) elif key == 'special': return self._handle_special(query, value, negated) else: @@ -97,26 +76,40 @@ class SearchExecutor(object): 'Unknown special token: %r. Available special tokens: %r.' % ( value, list(self._search_config.special_filters.keys()))) - def _handle_order(self, query, value, order): - if value in self._search_config.order_columns: - column, default_order = self._search_config.order_columns[value] - if order is None: - order = default_order - elif order == -1: - if default_order == self._search_config.ORDER_ASC: - order = self._search_config.ORDER_DESC - elif default_order == self._search_config.ORDER_DESC: - order = self._search_config.ORDER_ASC - else: - order = self._search_config.ORDER_ASC + def _handle_order(self, query, value, negated): + if value.count(',') == 0: + order_str = None + elif value.count(',') == 1: + value, order_str = value.split(',') + else: + raise errors.SearchError( + 'Too many commas in order search token.') + + if value not in self._search_config.order_columns: + raise errors.SearchError( + 'Unknown search order: %r. Available search orders: %r.' % ( + value, list(self._search_config.order_columns.keys()))) + + column, default_order = self._search_config.order_columns[value] + if order_str == 'asc': + order = self._search_config.ORDER_ASC + elif order_str == 'desc': + order = self._search_config.ORDER_DESC + elif order_str is None: + order = default_order + else: + raise errors.SearchError( + 'Unknown search direction: %r.' % order_str) + if negated: if order == self._search_config.ORDER_ASC: - column = column.asc() - else: - column = column.desc() - return query.order_by(column) - raise errors.SearchError( - 'Unknown search order: %r. Available search orders: %r.' % ( - value, list(self._search_config.order_columns.keys()))) + order = self._search_config.ORDER_DESC + elif order == self._search_config.ORDER_DESC: + order = self._search_config.ORDER_ASC + if order == self._search_config.ORDER_ASC: + column = column.asc() + elif order == self._search_config.ORDER_DESC: + column = column.desc() + return query.order_by(column) def _create_criterion(self, value, negated): if '..' in value: diff --git a/server/szurubooru/search/tag_search_config.py b/server/szurubooru/search/tag_search_config.py new file mode 100644 index 0000000..080538f --- /dev/null +++ b/server/szurubooru/search/tag_search_config.py @@ -0,0 +1,97 @@ +import sqlalchemy +from sqlalchemy.sql.expression import func +from szurubooru import db +from szurubooru.search.base_search_config import BaseSearchConfig + +class TagSearchConfig(BaseSearchConfig): + def __init__(self): + self._session = None + + def create_query(self, session): + self._session = session + return session.query(db.Tag) + + def finalize_query(self, query): + return query.order_by(self._first_name_subquery.asc()) + + @property + def anonymous_filter(self): + return self._name_filter + + @property + def special_filters(self): + return {} + + @property + def named_filters(self): + return { + 'name': self._name_filter, + 'category': self._create_str_filter(db.Tag.category), + 'creation-date': self._create_date_filter(db.Tag.creation_time), + 'creation-time': self._create_date_filter(db.Tag.creation_time), + 'last-edit-date': self._create_date_filter(db.Tag.last_edit_time), + 'last-edit-time': self._create_date_filter(db.Tag.last_edit_time), + 'edit-date': self._create_date_filter(db.Tag.last_edit_time), + 'edit-time': self._create_date_filter(db.Tag.last_edit_time), + 'usages': self._create_num_filter(db.Tag.post_count), + 'usage-count': self._create_num_filter(db.Tag.post_count), + 'post-count': self._create_num_filter(db.Tag.post_count), + 'suggestion-count': self._suggestion_count_filter, + 'implication-count': self._implication_count_filter, + } + + @property + def order_columns(self): + return { + 'random': (func.random(), None), + 'name': (self._first_name_subquery, self.ORDER_ASC), + 'category': (db.Tag.category, self.ORDER_ASC), + 'creation-date': (db.Tag.creation_time, self.ORDER_DESC), + 'creation-time': (db.Tag.creation_time, self.ORDER_DESC), + 'last-edit-date': (db.Tag.last_edit_time, self.ORDER_DESC), + 'last-edit-time': (db.Tag.last_edit_time, self.ORDER_DESC), + 'edit-date': (db.Tag.last_edit_time, self.ORDER_DESC), + 'edit-time': (db.Tag.last_edit_time, self.ORDER_DESC), + 'usages': (db.Tag.post_count, self.ORDER_DESC), + 'usage-count': (db.Tag.post_count, self.ORDER_DESC), + 'post-count': (db.Tag.post_count, self.ORDER_DESC), + 'suggestion-count': + (self._suggestion_count_subquery, self.ORDER_DESC), + 'implication-count': + (self._implication_count_subquery, self.ORDER_DESC), + } + + def _name_filter(self, query, criterion): + str_filter = self._create_str_filter(db.TagName.name) + return query.filter( + db.Tag.tag_id.in_( + str_filter(self._session.query(db.TagName.tag_id), criterion))) + + def _suggestion_count_filter(self, query, criterion): + return query.filter( + self._apply_num_criterion_to_column( + self._suggestion_count_subquery, criterion)) + + def _implication_count_filter(self, query, criterion): + return query.filter( + self._apply_num_criterion_to_column( + self._implication_count_subquery, criterion)) + + @property + def _first_name_subquery(self): + return sqlalchemy.select([db.TagName.name]) \ + .limit(1) \ + .where(db.TagName.tag_id == db.Tag.tag_id) \ + .as_scalar() + + @property + def _suggestion_count_subquery(self): + return sqlalchemy.select([func.count(db.TagSuggestion.child_id)]) \ + .where(db.TagSuggestion.parent_id == db.Tag.tag_id) \ + .as_scalar() + + @property + def _implication_count_subquery(self): + return sqlalchemy.select([func.count(1)]) \ + .where(db.TagImplication.parent_id == db.Tag.tag_id) \ + .as_scalar() diff --git a/server/szurubooru/search/user_search_config.py b/server/szurubooru/search/user_search_config.py index 09798d2..b42be41 100644 --- a/server/szurubooru/search/user_search_config.py +++ b/server/szurubooru/search/user_search_config.py @@ -34,7 +34,7 @@ class UserSearchConfig(BaseSearchConfig): @property def order_columns(self): return { - 'random': func.random(), + 'random': (None, func.random()), 'name': (db.User.name, self.ORDER_ASC), 'creation-date': (db.User.creation_time, self.ORDER_DESC), 'creation-time': (db.User.creation_time, self.ORDER_DESC), diff --git a/server/szurubooru/tests/search/test_tag_search_config.py b/server/szurubooru/tests/search/test_tag_search_config.py new file mode 100644 index 0000000..c620962 --- /dev/null +++ b/server/szurubooru/tests/search/test_tag_search_config.py @@ -0,0 +1,176 @@ +import datetime +import pytest +from szurubooru import db, errors, search + +@pytest.fixture +def executor(session): + search_config = search.TagSearchConfig() + return search.SearchExecutor(search_config) + +@pytest.fixture +def verify_unpaged(session, executor): + def verify(input, expected_tag_names): + actual_count, actual_tags = executor.execute( + session, input, page=1, page_size=100) + actual_tag_names = [u.names[0].name for u in actual_tags] + assert actual_count == len(expected_tag_names) + assert actual_tag_names == expected_tag_names + return verify + +@pytest.mark.parametrize('input,expected_tag_names', [ + ('creation-time:2014', ['t1', 't2']), + ('creation-date:2014', ['t1', 't2']), + ('-creation-time:2014', ['t3']), + ('-creation-date:2014', ['t3']), + ('creation-time:2014..2014-06', ['t1', 't2']), + ('creation-time:2014-06..2015-01-01', ['t2', 't3']), + ('creation-time:2014-06..', ['t2', 't3']), + ('creation-time:..2014-06', ['t1', 't2']), + ('-creation-time:2014..2014-06', ['t3']), + ('-creation-time:2014-06..2015-01-01', ['t1']), + ('creation-date:2014..2014-06', ['t1', 't2']), + ('creation-date:2014-06..2015-01-01', ['t2', 't3']), + ('creation-date:2014-06..', ['t2', 't3']), + ('creation-date:..2014-06', ['t1', 't2']), + ('-creation-date:2014..2014-06', ['t3']), + ('-creation-date:2014-06..2015-01-01', ['t1']), + ('creation-time:2014-01,2015', ['t1', 't3']), + ('creation-date:2014-01,2015', ['t1', 't3']), + ('-creation-time:2014-01,2015', ['t2']), + ('-creation-date:2014-01,2015', ['t2']), +]) +def test_filter_by_creation_time( + verify_unpaged, session, tag_factory, input, expected_tag_names): + tag1 = tag_factory(names=['t1']) + tag2 = tag_factory(names=['t2']) + tat3 = tag_factory(names=['t3']) + tag1.creation_time = datetime.datetime(2014, 1, 1) + tag2.creation_time = datetime.datetime(2014, 6, 1) + tat3.creation_time = datetime.datetime(2015, 1, 1) + session.add_all([tag1, tag2, tat3]) + verify_unpaged(input, expected_tag_names) + +@pytest.mark.parametrize('input,expected_tag_names', [ + ('name:tag1', ['tag1']), + ('name:tag2', ['tag2']), + ('name:none', []), + ('name:', []), + ('name:*1', ['tag1']), + ('name:*2', ['tag2']), + ('name:*', ['tag1', 'tag2', 'tag3', 'tag4']), + ('name:t*', ['tag1', 'tag2', 'tag3', 'tag4']), + ('name:*a*', ['tag1', 'tag2', 'tag3', 'tag4']), + ('name:*!*', []), + ('name:!*', []), + ('name:*!', []), + ('-name:tag1', ['tag2', 'tag3', 'tag4']), + ('-name:tag2', ['tag1', 'tag3', 'tag4']), + ('name:tag1,tag2', ['tag1', 'tag2']), + ('-name:tag1,tag3', ['tag2', 'tag4']), + ('name:tag4', ['tag4']), + ('name:tag4,tag5', ['tag4']), +]) +def test_filter_by_name( + session, verify_unpaged, tag_factory, input, expected_tag_names): + session.add(tag_factory(names=['tag1'])) + session.add(tag_factory(names=['tag2'])) + session.add(tag_factory(names=['tag3'])) + session.add(tag_factory(names=['tag4', 'tag5', 'tag6'])) + verify_unpaged(input, expected_tag_names) + +@pytest.mark.parametrize('input,expected_tag_names', [ + ('', ['t1', 't2']), + ('t1', ['t1']), + ('t2', ['t2']), + ('t1,t2', ['t1', 't2']), +]) +def test_anonymous( + session, verify_unpaged, tag_factory, input, expected_tag_names): + session.add(tag_factory(names=['t1'])) + session.add(tag_factory(names=['t2'])) + verify_unpaged(input, expected_tag_names) + +@pytest.mark.parametrize('input,expected_tag_names', [ + ('', ['t1', 't2']), + ('order:name', ['t1', 't2']), + ('-order:name', ['t2', 't1']), + ('order:name,asc', ['t1', 't2']), + ('order:name,desc', ['t2', 't1']), + ('-order:name,asc', ['t2', 't1']), + ('-order:name,desc', ['t1', 't2']), +]) +def test_order_by_name( + session, verify_unpaged, tag_factory, input, expected_tag_names): + session.add(tag_factory(names=['t2'])) + session.add(tag_factory(names=['t1'])) + verify_unpaged(input, expected_tag_names) + +@pytest.mark.parametrize('input,expected_user_names', [ + ('', ['t1', 't2', 't3']), + ('order:creation-date', ['t3', 't2', 't1']), + ('order:creation-time', ['t3', 't2', 't1']), +]) +def test_order_by_creation_time( + session, verify_unpaged, tag_factory, input, expected_user_names): + tag1 = tag_factory(names=['t1']) + tag2 = tag_factory(names=['t2']) + tag3 = tag_factory(names=['t3']) + tag1.creation_time = datetime.datetime(1991, 1, 1) + tag2.creation_time = datetime.datetime(1991, 1, 2) + tag3.creation_time = datetime.datetime(1991, 1, 3) + session.add_all([tag3, tag1, tag2]) + verify_unpaged(input, expected_user_names) + +@pytest.mark.parametrize('input,expected_tag_names', [ + ('order:suggestion-count', ['t1', 't2', 'sug1', 'sug2', 'sug3']), +]) +def test_order_by_suggestion_count( + session, verify_unpaged, tag_factory, input, expected_tag_names): + sug1 = tag_factory(names=['sug1']) + sug2 = tag_factory(names=['sug2']) + sug3 = tag_factory(names=['sug3']) + tag1 = tag_factory(names=['t1']) + tag2 = tag_factory(names=['t2']) + session.add_all([sug1, sug3, tag2, sug2, tag1]) + session.commit() + tag1.suggestions.append(db.TagSuggestion(tag1.tag_id, sug1.tag_id)) + tag1.suggestions.append(db.TagSuggestion(tag1.tag_id, sug2.tag_id)) + tag2.suggestions.append(db.TagSuggestion(tag2.tag_id, sug3.tag_id)) + verify_unpaged(input, expected_tag_names) + +@pytest.mark.parametrize('input,expected_tag_names', [ + ('order:implication-count', ['t1', 't2', 'sug1', 'sug2', 'sug3']), +]) +def test_order_by_implication_count( + session, verify_unpaged, tag_factory, input, expected_tag_names): + sug1 = tag_factory(names=['sug1']) + sug2 = tag_factory(names=['sug2']) + sug3 = tag_factory(names=['sug3']) + tag1 = tag_factory(names=['t1']) + tag2 = tag_factory(names=['t2']) + session.add_all([sug1, sug3, tag2, sug2, tag1]) + session.commit() + tag1.implications.append(db.TagImplication(tag1.tag_id, sug1.tag_id)) + tag1.implications.append(db.TagImplication(tag1.tag_id, sug2.tag_id)) + tag2.implications.append(db.TagImplication(tag2.tag_id, sug3.tag_id)) + verify_unpaged(input, expected_tag_names) + +def test_filter_by_relation_count(session, verify_unpaged, tag_factory): + sug1 = tag_factory(names=['sug1']) + sug2 = tag_factory(names=['sug2']) + imp1 = tag_factory(names=['imp1']) + tag1 = tag_factory(names=['t1']) + tag2 = tag_factory(names=['t2']) + session.add_all([sug1, tag1, sug2, imp1, tag2]) + session.commit() + session.add_all([ + db.TagSuggestion(tag1.tag_id, sug1.tag_id), + db.TagSuggestion(tag1.tag_id, sug2.tag_id), + db.TagImplication(tag2.tag_id, imp1.tag_id)]) + session.commit() + verify_unpaged('suggestion-count:0', ['imp1', 'sug1', 'sug2', 't2']) + verify_unpaged('suggestion-count:1', []) + verify_unpaged('suggestion-count:2', ['t1']) + verify_unpaged('implication-count:0', ['imp1', 'sug1', 'sug2', 't1']) + verify_unpaged('implication-count:1', ['t2']) + verify_unpaged('implication-count:2', []) diff --git a/server/szurubooru/tests/search/test_user_search_config.py b/server/szurubooru/tests/search/test_user_search_config.py index 55a3a86..645b9d9 100644 --- a/server/szurubooru/tests/search/test_user_search_config.py +++ b/server/szurubooru/tests/search/test_user_search_config.py @@ -140,13 +140,14 @@ def test_order_by_name( @pytest.mark.parametrize('input,expected_user_names', [ ('', ['u1', 'u2', 'u3']), ('order:creation-date', ['u3', 'u2', 'u1']), + ('order:creation-time', ['u3', 'u2', 'u1']), ('-order:creation-date', ['u1', 'u2', 'u3']), ('order:creation-date,asc', ['u1', 'u2', 'u3']), ('order:creation-date,desc', ['u3', 'u2', 'u1']), ('-order:creation-date,asc', ['u3', 'u2', 'u1']), ('-order:creation-date,desc', ['u1', 'u2', 'u3']), ]) -def test_order_by_name( +def test_order_by_creation_time( session, verify_unpaged, input, expected_user_names, user_factory): user1 = user_factory(name='u1') user2 = user_factory(name='u2') @@ -157,6 +158,38 @@ def test_order_by_name( session.add_all([user3, user1, user2]) verify_unpaged(input, expected_user_names) +@pytest.mark.parametrize('input,expected_user_names', [ + ('', ['u1', 'u2', 'u3']), + ('order:last-login-date', ['u3', 'u2', 'u1']), + ('order:last-login-time', ['u3', 'u2', 'u1']), + ('order:login-date', ['u3', 'u2', 'u1']), + ('order:login-time', ['u3', 'u2', 'u1']), +]) +def test_order_by_name( + session, verify_unpaged, input, expected_user_names, user_factory): + user1 = user_factory(name='u1') + user2 = user_factory(name='u2') + user3 = user_factory(name='u3') + user1.last_login_time = datetime.datetime(1991, 1, 1) + user2.last_login_time = datetime.datetime(1991, 1, 2) + user3.last_login_time = datetime.datetime(1991, 1, 3) + session.add_all([user3, user1, user2]) + verify_unpaged(input, expected_user_names) + +def test_random_order(session, executor, user_factory): + user1 = user_factory(name='u1') + user2 = user_factory(name='u2') + user3 = user_factory(name='u3') + session.add_all([user3, user1, user2]) + actual_count, actual_users = executor.execute( + session, 'order:random', page=1, page_size=100) + actual_user_names = [u.name for u in actual_users] + assert actual_count == 3 + assert len(actual_user_names) == 3 + assert 'u1' in actual_user_names + assert 'u2' in actual_user_names + assert 'u3' in actual_user_names + @pytest.mark.parametrize('input,expected_error', [ ('creation-date:..', errors.SearchError), ('creation-date:bad..', errors.ValidationError),