diff --git a/server/szurubooru/api/context.py b/server/szurubooru/api/context.py index e66873c7..a21cce02 100644 --- a/server/szurubooru/api/context.py +++ b/server/szurubooru/api/context.py @@ -2,8 +2,8 @@ import falcon from szurubooru import errors from szurubooru.func import net -def _lower_first(input): - return input[0].lower() + input[1:] +def _lower_first(source): + return source[0].lower() + source[1:] def _param_wrapper(func): def wrapper(self, name, required=False, default=None, **kwargs): @@ -11,10 +11,10 @@ def _param_wrapper(func): value = self.input[name] try: value = func(self, value, **kwargs) - except errors.InvalidParameterError as e: + except errors.InvalidParameterError as ex: raise errors.InvalidParameterError( 'Parameter %r is invalid: %s' % ( - name, _lower_first(str(e)))) + name, _lower_first(str(ex)))) return value if not required: return default diff --git a/server/szurubooru/search/configs/base_search_config.py b/server/szurubooru/search/configs/base_search_config.py index dfbe4b0f..1dc9d4bd 100644 --- a/server/szurubooru/search/configs/base_search_config.py +++ b/server/szurubooru/search/configs/base_search_config.py @@ -1,10 +1,4 @@ -import sqlalchemy -from szurubooru import db, errors -from szurubooru.func import util -from szurubooru.search import criteria, tokens - -def wildcard_transformer(value): - return value.replace('*', '%') +from szurubooru.search import tokens class BaseSearchConfig(object): SORT_ASC = tokens.SortToken.SORT_ASC @@ -34,127 +28,3 @@ class BaseSearchConfig(object): @property def sort_columns(self): return {} - - @staticmethod - def _apply_num_criterion_to_column(column, criterion): - ''' - Decorate SQLAlchemy filter on given column using supplied criterion. - ''' - try: - if isinstance(criterion, criteria.PlainCriterion): - expr = column == int(criterion.value) - elif isinstance(criterion, criteria.ArrayCriterion): - expr = column.in_(int(value) for value in criterion.values) - elif isinstance(criterion, criteria.RangedCriterion): - assert criterion.min_value != '' \ - or criterion.max_value != '' - if criterion.min_value != '' and criterion.max_value != '': - expr = column.between( - int(criterion.min_value), int(criterion.max_value)) - elif criterion.min_value != '': - expr = column >= int(criterion.min_value) - elif criterion.max_value != '': - expr = column <= int(criterion.max_value) - else: - assert False - except ValueError: - raise errors.SearchError( - 'Criterion value %r must be a number.' % (criterion,)) - return expr - - @staticmethod - def _create_num_filter(column): - def wrapper(query, criterion, negated): - expr = BaseSearchConfig._apply_num_criterion_to_column( - column, criterion) - if negated: - expr = ~expr - return query.filter(expr) - return wrapper - - @staticmethod - def _apply_str_criterion_to_column( - column, criterion, transformer=wildcard_transformer): - ''' - Decorate SQLAlchemy filter on given column using supplied criterion. - ''' - if isinstance(criterion, criteria.PlainCriterion): - expr = column.ilike(transformer(criterion.value)) - elif isinstance(criterion, criteria.ArrayCriterion): - expr = sqlalchemy.sql.false() - for value in criterion.values: - expr = expr | column.ilike(transformer(value)) - elif isinstance(criterion, criteria.RangedCriterion): - raise errors.SearchError( - 'Composite token %r is invalid in this context.' % (criterion,)) - else: - assert False - return expr - - @staticmethod - def _create_str_filter(column, transformer=wildcard_transformer): - def wrapper(query, criterion, negated): - expr = BaseSearchConfig._apply_str_criterion_to_column( - column, criterion, transformer) - if negated: - expr = ~expr - return query.filter(expr) - return wrapper - - @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.PlainCriterion): - min_date, max_date = util.parse_time_range(criterion.value) - expr = column.between(min_date, max_date) - elif isinstance(criterion, criteria.ArrayCriterion): - expr = sqlalchemy.sql.false() - for value in criterion.values: - min_date, max_date = util.parse_time_range(value) - expr = expr | column.between(min_date, max_date) - elif isinstance(criterion, criteria.RangedCriterion): - assert criterion.min_value or criterion.max_value - if criterion.min_value and criterion.max_value: - min_date = util.parse_time_range(criterion.min_value)[0] - max_date = util.parse_time_range(criterion.max_value)[1] - expr = column.between(min_date, max_date) - elif criterion.min_value: - min_date = util.parse_time_range(criterion.min_value)[0] - expr = column >= min_date - elif criterion.max_value: - max_date = util.parse_time_range(criterion.max_value)[1] - expr = column <= max_date - else: - assert False - return expr - - @staticmethod - def _create_date_filter(column): - def wrapper(query, criterion, negated): - expr = BaseSearchConfig._apply_date_criterion_to_column( - column, criterion) - if negated: - expr = ~expr - return query.filter(expr) - return wrapper - - @staticmethod - def _create_subquery_filter( - left_id_column, - right_id_column, - filter_column, - filter_factory, - subquery_decorator=None): - filter_func = filter_factory(filter_column) - def wrapper(query, criterion, negated): - subquery = db.session.query(right_id_column.label('foreign_id')) - if subquery_decorator: - subquery = subquery_decorator(subquery) - subquery = subquery.options(sqlalchemy.orm.lazyload('*')) - subquery = filter_func(subquery, criterion, negated) - subquery = subquery.subquery('t') - return query.filter(left_id_column.in_(subquery)) - return wrapper diff --git a/server/szurubooru/search/configs/comment_search_config.py b/server/szurubooru/search/configs/comment_search_config.py index bc6eea70..b921492f 100644 --- a/server/szurubooru/search/configs/comment_search_config.py +++ b/server/szurubooru/search/configs/comment_search_config.py @@ -1,5 +1,6 @@ from sqlalchemy.sql.expression import func from szurubooru import db +from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import BaseSearchConfig class CommentSearchConfig(BaseSearchConfig): @@ -11,22 +12,22 @@ class CommentSearchConfig(BaseSearchConfig): @property def anonymous_filter(self): - return self._create_str_filter(db.Comment.text) + return search_util.create_str_filter(db.Comment.text) @property def named_filters(self): return { - 'id': self._create_num_filter(db.Comment.comment_id), - 'post': self._create_num_filter(db.Comment.post_id), - 'user': self._create_str_filter(db.User.name), - 'author': self._create_str_filter(db.User.name), - 'text': self._create_str_filter(db.Comment.text), - 'creation-date': self._create_date_filter(db.Comment.creation_time), - 'creation-time': self._create_date_filter(db.Comment.creation_time), - 'last-edit-date': self._create_date_filter(db.Comment.last_edit_time), - 'last-edit-time': self._create_date_filter(db.Comment.last_edit_time), - 'edit-date': self._create_date_filter(db.Comment.last_edit_time), - 'edit-time': self._create_date_filter(db.Comment.last_edit_time), + 'id': search_util.create_num_filter(db.Comment.comment_id), + 'post': search_util.create_num_filter(db.Comment.post_id), + 'user': search_util.create_str_filter(db.User.name), + 'author': search_util.create_str_filter(db.User.name), + 'text': search_util.create_str_filter(db.Comment.text), + 'creation-date': search_util.create_date_filter(db.Comment.creation_time), + 'creation-time': search_util.create_date_filter(db.Comment.creation_time), + 'last-edit-date': search_util.create_date_filter(db.Comment.last_edit_time), + 'last-edit-time': search_util.create_date_filter(db.Comment.last_edit_time), + 'edit-date': search_util.create_date_filter(db.Comment.last_edit_time), + 'edit-time': search_util.create_date_filter(db.Comment.last_edit_time), } @property diff --git a/server/szurubooru/search/configs/post_search_config.py b/server/szurubooru/search/configs/post_search_config.py index 85c25e38..0f4adafd 100644 --- a/server/szurubooru/search/configs/post_search_config.py +++ b/server/szurubooru/search/configs/post_search_config.py @@ -3,6 +3,7 @@ from sqlalchemy.sql.expression import func from szurubooru import db, errors from szurubooru.func import util from szurubooru.search import criteria, tokens +from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import BaseSearchConfig def _enum_transformer(available_values, value): @@ -45,7 +46,7 @@ def _create_score_filter(score): user_alias = aliased(db.User) score_alias = aliased(db.PostScore) expr = score_alias.score == score - expr = expr & BaseSearchConfig._apply_str_criterion_to_column( + expr = expr & search_util.apply_str_criterion_to_column( user_alias.name, criterion) if negated: expr = ~expr @@ -106,69 +107,69 @@ class PostSearchConfig(BaseSearchConfig): @property def anonymous_filter(self): - return self._create_subquery_filter( + return search_util.create_subquery_filter( db.Post.post_id, db.PostTag.post_id, db.TagName.name, - self._create_str_filter, + search_util.create_str_filter, lambda subquery: subquery.join(db.Tag).join(db.TagName)) @property def named_filters(self): return util.unalias_dict({ - 'id': self._create_num_filter(db.Post.post_id), - 'tag': self._create_subquery_filter( + 'id': search_util.create_num_filter(db.Post.post_id), + 'tag': search_util.create_subquery_filter( db.Post.post_id, db.PostTag.post_id, db.TagName.name, - self._create_str_filter, + search_util.create_str_filter, lambda subquery: subquery.join(db.Tag).join(db.TagName)), - 'score': self._create_num_filter(db.Post.score), + 'score': search_util.create_num_filter(db.Post.score), ('uploader', 'upload', 'submit'): - self._create_subquery_filter( + search_util.create_subquery_filter( db.Post.user_id, db.User.user_id, db.User.name, - self._create_str_filter), - 'comment': self._create_subquery_filter( + search_util.create_str_filter), + 'comment': search_util.create_subquery_filter( db.Post.post_id, db.Comment.post_id, db.User.name, - self._create_str_filter, + search_util.create_str_filter, lambda subquery: subquery.join(db.User)), - 'fav': self._create_subquery_filter( + 'fav': search_util.create_subquery_filter( db.Post.post_id, db.PostFavorite.post_id, db.User.name, - self._create_str_filter, + search_util.create_str_filter, lambda subquery: subquery.join(db.User)), 'liked': _create_score_filter(1), 'disliked': _create_score_filter(-1), - 'tag-count': self._create_num_filter(db.Post.tag_count), - 'comment-count': self._create_num_filter(db.Post.comment_count), - 'fav-count': self._create_num_filter(db.Post.favorite_count), - 'note-count': self._create_num_filter(db.Post.note_count), - 'feature-count': self._create_num_filter(db.Post.feature_count), - 'type': self._create_str_filter(db.Post.type, _type_transformer), - 'file-size': self._create_num_filter(db.Post.file_size), + 'tag-count': search_util.create_num_filter(db.Post.tag_count), + 'comment-count': search_util.create_num_filter(db.Post.comment_count), + 'fav-count': search_util.create_num_filter(db.Post.favorite_count), + 'note-count': search_util.create_num_filter(db.Post.note_count), + 'feature-count': search_util.create_num_filter(db.Post.feature_count), + 'type': search_util.create_str_filter(db.Post.type, _type_transformer), + 'file-size': search_util.create_num_filter(db.Post.file_size), ('image-width', 'width'): - self._create_num_filter(db.Post.canvas_width), + search_util.create_num_filter(db.Post.canvas_width), ('image-height', 'height'): - self._create_num_filter(db.Post.canvas_height), + search_util.create_num_filter(db.Post.canvas_height), ('image-area', 'area'): - self._create_num_filter(db.Post.canvas_area), + search_util.create_num_filter(db.Post.canvas_area), ('creation-date', 'creation-time', 'date', 'time'): - self._create_date_filter(db.Post.creation_time), + search_util.create_date_filter(db.Post.creation_time), ('last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'): - self._create_date_filter(db.Post.last_edit_time), + search_util.create_date_filter(db.Post.last_edit_time), ('comment-date', 'comment-time'): - self._create_date_filter(db.Post.last_comment_edit_time), + search_util.create_date_filter(db.Post.last_comment_edit_time), ('fav-date', 'fav-time'): - self._create_date_filter(db.Post.last_favorite_time), + search_util.create_date_filter(db.Post.last_favorite_time), ('feature-date', 'feature-time'): - self._create_date_filter(db.Post.last_feature_time), + search_util.create_date_filter(db.Post.last_feature_time), ('safety', 'rating'): - self._create_str_filter(db.Post.safety, _safety_transformer), + search_util.create_str_filter(db.Post.safety, _safety_transformer), }) @property diff --git a/server/szurubooru/search/configs/snapshot_search_config.py b/server/szurubooru/search/configs/snapshot_search_config.py index a99aee73..a5e7f9de 100644 --- a/server/szurubooru/search/configs/snapshot_search_config.py +++ b/server/szurubooru/search/configs/snapshot_search_config.py @@ -1,4 +1,5 @@ from szurubooru import db +from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import BaseSearchConfig class SnapshotSearchConfig(BaseSearchConfig): @@ -11,10 +12,10 @@ class SnapshotSearchConfig(BaseSearchConfig): @property def named_filters(self): return { - 'type': self._create_str_filter(db.Snapshot.resource_type), - 'id': self._create_str_filter(db.Snapshot.resource_repr), - 'date': self._create_date_filter(db.Snapshot.creation_time), - 'time': self._create_date_filter(db.Snapshot.creation_time), - 'operation': self._create_str_filter(db.Snapshot.operation), - 'user': self._create_str_filter(db.User.name), + 'type': search_util.create_str_filter(db.Snapshot.resource_type), + 'id': search_util.create_str_filter(db.Snapshot.resource_repr), + 'date': search_util.create_date_filter(db.Snapshot.creation_time), + 'time': search_util.create_date_filter(db.Snapshot.creation_time), + 'operation': search_util.create_str_filter(db.Snapshot.operation), + 'user': search_util.create_str_filter(db.User.name), } diff --git a/server/szurubooru/search/configs/tag_search_config.py b/server/szurubooru/search/configs/tag_search_config.py index 4422d875..05bbbcd4 100644 --- a/server/szurubooru/search/configs/tag_search_config.py +++ b/server/szurubooru/search/configs/tag_search_config.py @@ -2,6 +2,7 @@ from sqlalchemy.orm import subqueryload from sqlalchemy.sql.expression import func from szurubooru import db from szurubooru.func import util +from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import BaseSearchConfig class TagSearchConfig(BaseSearchConfig): @@ -23,33 +24,35 @@ class TagSearchConfig(BaseSearchConfig): @property def anonymous_filter(self): - return self._create_subquery_filter( + return search_util.create_subquery_filter( db.Tag.tag_id, db.TagName.tag_id, db.TagName.name, - self._create_str_filter) + search_util.create_str_filter) @property def named_filters(self): return util.unalias_dict({ - 'name': self._create_subquery_filter( + 'name': search_util.create_subquery_filter( db.Tag.tag_id, db.TagName.tag_id, db.TagName.name, - self._create_str_filter), - 'category': self._create_subquery_filter( + search_util.create_str_filter), + 'category': search_util.create_subquery_filter( db.Tag.category_id, db.TagCategory.tag_category_id, db.TagCategory.name, - self._create_str_filter), + search_util.create_str_filter), ('creation-date', 'creation-time'): - self._create_date_filter(db.Tag.creation_time), + search_util.create_date_filter(db.Tag.creation_time), ('last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'): - self._create_date_filter(db.Tag.last_edit_time), + search_util.create_date_filter(db.Tag.last_edit_time), ('usage-count', 'post-count', 'usages'): - self._create_num_filter(db.Tag.post_count), - 'suggestion-count': self._create_num_filter(db.Tag.suggestion_count), - 'implication-count': self._create_num_filter(db.Tag.implication_count), + search_util.create_num_filter(db.Tag.post_count), + 'suggestion-count': + search_util.create_num_filter(db.Tag.suggestion_count), + 'implication-count': + search_util.create_num_filter(db.Tag.implication_count), }) @property diff --git a/server/szurubooru/search/configs/user_search_config.py b/server/szurubooru/search/configs/user_search_config.py index 5d50be10..36468919 100644 --- a/server/szurubooru/search/configs/user_search_config.py +++ b/server/szurubooru/search/configs/user_search_config.py @@ -1,5 +1,6 @@ from sqlalchemy.sql.expression import func from szurubooru import db +from szurubooru.search.configs import util as search_util from szurubooru.search.configs.base_search_config import BaseSearchConfig class UserSearchConfig(BaseSearchConfig): @@ -13,18 +14,18 @@ class UserSearchConfig(BaseSearchConfig): @property def anonymous_filter(self): - return self._create_str_filter(db.User.name) + return search_util.create_str_filter(db.User.name) @property def named_filters(self): return { - '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), - 'last-login-time': self._create_date_filter(db.User.last_login_time), - 'login-date': self._create_date_filter(db.User.last_login_time), - 'login-time': self._create_date_filter(db.User.last_login_time), + 'name': search_util.create_str_filter(db.User.name), + 'creation-date': search_util.create_date_filter(db.User.creation_time), + 'creation-time': search_util.create_date_filter(db.User.creation_time), + 'last-login-date': search_util.create_date_filter(db.User.last_login_time), + 'last-login-time': search_util.create_date_filter(db.User.last_login_time), + 'login-date': search_util.create_date_filter(db.User.last_login_time), + 'login-time': search_util.create_date_filter(db.User.last_login_time), } @property diff --git a/server/szurubooru/search/configs/util.py b/server/szurubooru/search/configs/util.py new file mode 100644 index 00000000..5e770ae3 --- /dev/null +++ b/server/szurubooru/search/configs/util.py @@ -0,0 +1,124 @@ +import sqlalchemy +from szurubooru import db, errors +from szurubooru.func import util +from szurubooru.search import criteria + +def wildcard_transformer(value): + return value.replace('*', '%') + +def apply_num_criterion_to_column(column, criterion): + ''' + Decorate SQLAlchemy filter on given column using supplied criterion. + ''' + try: + if isinstance(criterion, criteria.PlainCriterion): + expr = column == int(criterion.value) + elif isinstance(criterion, criteria.ArrayCriterion): + expr = column.in_(int(value) for value in criterion.values) + elif isinstance(criterion, criteria.RangedCriterion): + assert criterion.min_value != '' \ + or criterion.max_value != '' + if criterion.min_value != '' and criterion.max_value != '': + expr = column.between( + int(criterion.min_value), int(criterion.max_value)) + elif criterion.min_value != '': + expr = column >= int(criterion.min_value) + elif criterion.max_value != '': + expr = column <= int(criterion.max_value) + else: + assert False + except ValueError: + raise errors.SearchError( + 'Criterion value %r must be a number.' % (criterion,)) + return expr + +def create_num_filter(column): + def wrapper(query, criterion, negated): + expr = apply_num_criterion_to_column( + column, criterion) + if negated: + expr = ~expr + return query.filter(expr) + return wrapper + +def apply_str_criterion_to_column( + column, criterion, transformer=wildcard_transformer): + ''' + Decorate SQLAlchemy filter on given column using supplied criterion. + ''' + if isinstance(criterion, criteria.PlainCriterion): + expr = column.ilike(transformer(criterion.value)) + elif isinstance(criterion, criteria.ArrayCriterion): + expr = sqlalchemy.sql.false() + for value in criterion.values: + expr = expr | column.ilike(transformer(value)) + elif isinstance(criterion, criteria.RangedCriterion): + raise errors.SearchError( + 'Composite token %r is invalid in this context.' % (criterion,)) + else: + assert False + return expr + +def create_str_filter(column, transformer=wildcard_transformer): + def wrapper(query, criterion, negated): + expr = apply_str_criterion_to_column( + column, criterion, transformer) + if negated: + expr = ~expr + return query.filter(expr) + return wrapper + +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.PlainCriterion): + min_date, max_date = util.parse_time_range(criterion.value) + expr = column.between(min_date, max_date) + elif isinstance(criterion, criteria.ArrayCriterion): + expr = sqlalchemy.sql.false() + for value in criterion.values: + min_date, max_date = util.parse_time_range(value) + expr = expr | column.between(min_date, max_date) + elif isinstance(criterion, criteria.RangedCriterion): + assert criterion.min_value or criterion.max_value + if criterion.min_value and criterion.max_value: + min_date = util.parse_time_range(criterion.min_value)[0] + max_date = util.parse_time_range(criterion.max_value)[1] + expr = column.between(min_date, max_date) + elif criterion.min_value: + min_date = util.parse_time_range(criterion.min_value)[0] + expr = column >= min_date + elif criterion.max_value: + max_date = util.parse_time_range(criterion.max_value)[1] + expr = column <= max_date + else: + assert False + return expr + +def create_date_filter(column): + def wrapper(query, criterion, negated): + expr = apply_date_criterion_to_column( + column, criterion) + if negated: + expr = ~expr + return query.filter(expr) + return wrapper + +def create_subquery_filter( + left_id_column, + right_id_column, + filter_column, + filter_factory, + subquery_decorator=None): + filter_func = filter_factory(filter_column) + def wrapper(query, criterion, negated): + subquery = db.session.query(right_id_column.label('foreign_id')) + if subquery_decorator: + subquery = subquery_decorator(subquery) + subquery = subquery.options(sqlalchemy.orm.lazyload('*')) + subquery = filter_func(subquery, criterion, negated) + subquery = subquery.subquery('t') + return query.filter(left_id_column.in_(subquery)) + return wrapper diff --git a/server/szurubooru/tests/search/test_comment_search_config.py b/server/szurubooru/tests/search/configs/test_comment_search_config.py similarity index 100% rename from server/szurubooru/tests/search/test_comment_search_config.py rename to server/szurubooru/tests/search/configs/test_comment_search_config.py diff --git a/server/szurubooru/tests/search/test_post_search_config.py b/server/szurubooru/tests/search/configs/test_post_search_config.py similarity index 100% rename from server/szurubooru/tests/search/test_post_search_config.py rename to server/szurubooru/tests/search/configs/test_post_search_config.py diff --git a/server/szurubooru/tests/search/test_tag_search_config.py b/server/szurubooru/tests/search/configs/test_tag_search_config.py similarity index 100% rename from server/szurubooru/tests/search/test_tag_search_config.py rename to server/szurubooru/tests/search/configs/test_tag_search_config.py diff --git a/server/szurubooru/tests/search/test_user_search_config.py b/server/szurubooru/tests/search/configs/test_user_search_config.py similarity index 100% rename from server/szurubooru/tests/search/test_user_search_config.py rename to server/szurubooru/tests/search/configs/test_user_search_config.py