server/users: add searching prototype
This commit is contained in:
parent
baf9b1d31a
commit
a157d2db0e
15 changed files with 503 additions and 7 deletions
|
@ -5,7 +5,8 @@ import types
|
||||||
def _bind_method(target, desired_method_name):
|
def _bind_method(target, desired_method_name):
|
||||||
actual_method = getattr(target, desired_method_name)
|
actual_method = getattr(target, desired_method_name)
|
||||||
def _wrapper_method(self, request, response, *args, **kwargs):
|
def _wrapper_method(self, request, response, *args, **kwargs):
|
||||||
request.context.result = actual_method(request.context, *args, **kwargs)
|
request.context.result = actual_method(
|
||||||
|
request, request.context, *args, **kwargs)
|
||||||
return types.MethodType(_wrapper_method, target)
|
return types.MethodType(_wrapper_method, target)
|
||||||
|
|
||||||
class BaseApi(object):
|
class BaseApi(object):
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
''' Users public API. '''
|
''' Users public API. '''
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from szurubooru.errors import IntegrityError, ValidationError
|
|
||||||
from szurubooru.api.base_api import BaseApi
|
from szurubooru.api.base_api import BaseApi
|
||||||
|
from szurubooru.errors import IntegrityError, ValidationError
|
||||||
|
from szurubooru.services.search import UserSearchConfig, SearchExecutor
|
||||||
|
|
||||||
def _serialize_user(authenticated_user, user):
|
def _serialize_user(authenticated_user, user):
|
||||||
ret = {
|
ret = {
|
||||||
|
@ -23,13 +24,23 @@ class UserListApi(BaseApi):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._auth_service = auth_service
|
self._auth_service = auth_service
|
||||||
self._user_service = user_service
|
self._user_service = user_service
|
||||||
|
self._search_executor = SearchExecutor(UserSearchConfig())
|
||||||
|
|
||||||
def get(self, context):
|
def get(self, request, context):
|
||||||
''' Retrieves a list of users. '''
|
''' Retrieves a list of users. '''
|
||||||
self._auth_service.verify_privilege(context.user, 'users:list')
|
self._auth_service.verify_privilege(context.user, 'users:list')
|
||||||
return {'message': 'Searching for users'}
|
query = request.get_param_as_string('query')
|
||||||
|
page = request.get_param_as_int('page', 1)
|
||||||
|
count, users = self._search_executor.execute(context.session, query, page)
|
||||||
|
return {
|
||||||
|
'query': query,
|
||||||
|
'page': page,
|
||||||
|
'page_size': self._search_executor.page_size,
|
||||||
|
'total': count,
|
||||||
|
'users': [_serialize_user(context.user, user) for user in users],
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, context):
|
def post(self, request, context):
|
||||||
''' Creates a new user. '''
|
''' Creates a new user. '''
|
||||||
self._auth_service.verify_privilege(context.user, 'users:create')
|
self._auth_service.verify_privilege(context.user, 'users:create')
|
||||||
|
|
||||||
|
@ -55,13 +66,13 @@ class UserDetailApi(BaseApi):
|
||||||
self._auth_service = auth_service
|
self._auth_service = auth_service
|
||||||
self._user_service = user_service
|
self._user_service = user_service
|
||||||
|
|
||||||
def get(self, context, user_name):
|
def get(self, request, context, user_name):
|
||||||
''' Retrieves an user. '''
|
''' Retrieves an user. '''
|
||||||
self._auth_service.verify_privilege(context.user, 'users:view')
|
self._auth_service.verify_privilege(context.user, 'users:view')
|
||||||
user = self._user_service.get_by_name(context.session, user_name)
|
user = self._user_service.get_by_name(context.session, user_name)
|
||||||
return {'user': _serialize_user(context.user, user)}
|
return {'user': _serialize_user(context.user, user)}
|
||||||
|
|
||||||
def put(self, context, user_name):
|
def put(self, request, context, user_name):
|
||||||
''' Updates an existing user. '''
|
''' Updates an existing user. '''
|
||||||
self._auth_service.verify_privilege(context.user, 'users:edit')
|
self._auth_service.verify_privilege(context.user, 'users:edit')
|
||||||
return {'message': 'Updating user ' + user_name}
|
return {'message': 'Updating user ' + user_name}
|
||||||
|
|
|
@ -8,18 +8,35 @@ import szurubooru.api
|
||||||
import szurubooru.config
|
import szurubooru.config
|
||||||
import szurubooru.middleware
|
import szurubooru.middleware
|
||||||
import szurubooru.services
|
import szurubooru.services
|
||||||
|
import szurubooru.services.search
|
||||||
import szurubooru.util
|
import szurubooru.util
|
||||||
from szurubooru.errors import *
|
from szurubooru.errors import *
|
||||||
|
|
||||||
class _CustomRequest(falcon.Request):
|
class _CustomRequest(falcon.Request):
|
||||||
context_type = szurubooru.util.dotdict
|
context_type = szurubooru.util.dotdict
|
||||||
|
|
||||||
|
def get_param_as_string(self, name, required=False, store=None, default=None):
|
||||||
|
params = self._params
|
||||||
|
if name in params:
|
||||||
|
param = params[name]
|
||||||
|
if isinstance(param, list):
|
||||||
|
param = ','.join(param)
|
||||||
|
if store is not None:
|
||||||
|
store[name] = param
|
||||||
|
return param
|
||||||
|
if not required:
|
||||||
|
return default
|
||||||
|
raise falcon.HTTPMissingParam(name)
|
||||||
|
|
||||||
def _on_auth_error(ex, request, response, params):
|
def _on_auth_error(ex, request, response, params):
|
||||||
raise falcon.HTTPForbidden('Authentication error', str(ex))
|
raise falcon.HTTPForbidden('Authentication error', str(ex))
|
||||||
|
|
||||||
def _on_validation_error(ex, request, response, params):
|
def _on_validation_error(ex, request, response, params):
|
||||||
raise falcon.HTTPBadRequest('Validation error', str(ex))
|
raise falcon.HTTPBadRequest('Validation error', str(ex))
|
||||||
|
|
||||||
|
def _on_search_error(ex, request, response, params):
|
||||||
|
raise falcon.HTTPBadRequest('Search error', str(ex))
|
||||||
|
|
||||||
def _on_integrity_error(ex, request, response, params):
|
def _on_integrity_error(ex, request, response, params):
|
||||||
raise falcon.HTTPConflict('Integrity violation', ex.args[0])
|
raise falcon.HTTPConflict('Integrity violation', ex.args[0])
|
||||||
|
|
||||||
|
@ -60,6 +77,7 @@ def create_app():
|
||||||
app.add_error_handler(szurubooru.errors.AuthError, _on_auth_error)
|
app.add_error_handler(szurubooru.errors.AuthError, _on_auth_error)
|
||||||
app.add_error_handler(szurubooru.errors.IntegrityError, _on_integrity_error)
|
app.add_error_handler(szurubooru.errors.IntegrityError, _on_integrity_error)
|
||||||
app.add_error_handler(szurubooru.errors.ValidationError, _on_validation_error)
|
app.add_error_handler(szurubooru.errors.ValidationError, _on_validation_error)
|
||||||
|
app.add_error_handler(szurubooru.errors.SearchError, _on_search_error)
|
||||||
|
|
||||||
app.add_route('/users/', user_list)
|
app.add_route('/users/', user_list)
|
||||||
app.add_route('/user/{user_name}', user)
|
app.add_route('/user/{user_name}', user)
|
||||||
|
|
|
@ -8,3 +8,6 @@ class IntegrityError(RuntimeError):
|
||||||
|
|
||||||
class ValidationError(RuntimeError):
|
class ValidationError(RuntimeError):
|
||||||
''' Validation error (e.g. trying to create user with invalid name) '''
|
''' Validation error (e.g. trying to create user with invalid name) '''
|
||||||
|
|
||||||
|
class SearchError(RuntimeError):
|
||||||
|
''' Search error (e.g. trying to use special: where it doesn't make sense) '''
|
||||||
|
|
2
server/szurubooru/services/search/__init__.py
Normal file
2
server/szurubooru/services/search/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from szurubooru.services.search.search_executor import SearchExecutor
|
||||||
|
from szurubooru.services.search.user_search_config import UserSearchConfig
|
98
server/szurubooru/services/search/base_search_config.py
Normal file
98
server/szurubooru/services/search/base_search_config.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
''' Exports BaseSearchConfig. '''
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
from szurubooru.errors import SearchError
|
||||||
|
from szurubooru.services.search.criteria import *
|
||||||
|
from szurubooru.util import parse_time_range
|
||||||
|
|
||||||
|
def _apply_criterion_to_column(
|
||||||
|
column, query, criterion, allow_composite=True, allow_ranged=True):
|
||||||
|
''' Decorates SQLAlchemy filter on given column using supplied criterion. '''
|
||||||
|
if isinstance(criterion, StringSearchCriterion):
|
||||||
|
filter = column == criterion.value
|
||||||
|
if criterion.negated:
|
||||||
|
filter = ~filter
|
||||||
|
return query.filter(filter)
|
||||||
|
elif isinstance(criterion, ArraySearchCriterion):
|
||||||
|
if not allow_composite:
|
||||||
|
raise SearchError(
|
||||||
|
'Composite token %r is invalid in this context.' % (criterion,))
|
||||||
|
filter = column.in_(criterion.values)
|
||||||
|
if criterion.negated:
|
||||||
|
filter = ~filter
|
||||||
|
return query.filter(filter)
|
||||||
|
elif isinstance(criterion, RangedSearchCriterion):
|
||||||
|
if not allow_ranged:
|
||||||
|
raise SearchError(
|
||||||
|
'Ranged token %r is invalid in this context.' % (criterion,))
|
||||||
|
filter = column.between(criterion.min_value, criterion.max_value)
|
||||||
|
if criterion.negated:
|
||||||
|
filter = ~filter
|
||||||
|
return query.filter(filter)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Invalid search type: %r.' % (criterion,))
|
||||||
|
|
||||||
|
def _apply_date_criterion_to_column(column, query, criterion):
|
||||||
|
'''
|
||||||
|
Decorates SQLAlchemy filter on given column using supplied criterion.
|
||||||
|
Parses the datetime inside the criterion.
|
||||||
|
'''
|
||||||
|
if isinstance(criterion, StringSearchCriterion):
|
||||||
|
min_date, max_date = parse_time_range(criterion.value)
|
||||||
|
filter = column.between(min_date, max_date)
|
||||||
|
if criterion.negated:
|
||||||
|
filter = ~filter
|
||||||
|
return query.filter(filter)
|
||||||
|
elif isinstance(criterion, ArraySearchCriterion):
|
||||||
|
result = query
|
||||||
|
filter = sqlalchemy.sql.false()
|
||||||
|
for value in criterion.values:
|
||||||
|
min_date, max_date = parse_time_range(value)
|
||||||
|
filter = filter | column.between(min_date, max_date)
|
||||||
|
if criterion.negated:
|
||||||
|
filter = ~filter
|
||||||
|
return query.filter(filter)
|
||||||
|
elif isinstance(criterion, RangedSearchCriterion):
|
||||||
|
assert criterion.min_value or criterion.max_value
|
||||||
|
if criterion.min_value and criterion.max_value:
|
||||||
|
min_date = parse_time_range(criterion.min_value)[0]
|
||||||
|
max_date = parse_time_range(criterion.max_value)[1]
|
||||||
|
filter = column.between(min_date, max_date)
|
||||||
|
elif criterion.min_value:
|
||||||
|
min_date = parse_time_range(criterion.min_value)[0]
|
||||||
|
filter = column >= min_date
|
||||||
|
elif criterion.max_value:
|
||||||
|
max_date = parse_time_range(criterion.max_value)[1]
|
||||||
|
filter = column <= max_date
|
||||||
|
if criterion.negated:
|
||||||
|
filter = ~filter
|
||||||
|
return query.filter(filter)
|
||||||
|
|
||||||
|
class BaseSearchConfig(object):
|
||||||
|
def create_query(self, session):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anonymous_filter(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def special_filters(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def named_filters(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
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_date_filter(self, column):
|
||||||
|
return lambda query, criterion: _apply_date_criterion_to_column(
|
||||||
|
column, query, criterion)
|
23
server/szurubooru/services/search/criteria.py
Normal file
23
server/szurubooru/services/search/criteria.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
class BaseSearchCriterion(object):
|
||||||
|
def __init__(self, original_text, negated):
|
||||||
|
self.original_text = original_text
|
||||||
|
self.negated = negated
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.original_text
|
||||||
|
|
||||||
|
class RangedSearchCriterion(BaseSearchCriterion):
|
||||||
|
def __init__(self, original_text, negated, min_value, max_value):
|
||||||
|
super().__init__(original_text, negated)
|
||||||
|
self.min_value = min_value
|
||||||
|
self.max_value = max_value
|
||||||
|
|
||||||
|
class StringSearchCriterion(BaseSearchCriterion):
|
||||||
|
def __init__(self, original_text, negated, value):
|
||||||
|
super().__init__(original_text, negated)
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
class ArraySearchCriterion(BaseSearchCriterion):
|
||||||
|
def __init__(self, original_text, negated, values):
|
||||||
|
super().__init__(original_text, negated)
|
||||||
|
self.values = values
|
120
server/szurubooru/services/search/search_executor.py
Normal file
120
server/szurubooru/services/search/search_executor.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
''' Exports SearchExecutor. '''
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sqlalchemy
|
||||||
|
from szurubooru.errors import SearchError
|
||||||
|
from szurubooru.services.search.criteria import *
|
||||||
|
|
||||||
|
class SearchExecutor(object):
|
||||||
|
ORDER_DESC = 1
|
||||||
|
ORDER_ASC = 2
|
||||||
|
|
||||||
|
'''
|
||||||
|
Class for search parsing and execution. Handles plaintext parsing and
|
||||||
|
delegates sqlalchemy filter decoration to SearchConfig instances.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, search_config):
|
||||||
|
self.page_size = 100
|
||||||
|
self._search_config = search_config
|
||||||
|
|
||||||
|
def execute(self, session, query_text, page):
|
||||||
|
'''
|
||||||
|
Parse input and return tuple containing total record count and filtered
|
||||||
|
entities.
|
||||||
|
'''
|
||||||
|
filter_query = self._prepare(session, query_text)
|
||||||
|
entities = filter_query \
|
||||||
|
.offset((page - 1) * self.page_size).limit(self.page_size).all()
|
||||||
|
count_query = filter_query.statement \
|
||||||
|
.with_only_columns([sqlalchemy.func.count()]).order_by(None)
|
||||||
|
count = filter_query.session.execute(count_query).scalar()
|
||||||
|
return (count, entities)
|
||||||
|
|
||||||
|
def _prepare(self, session, query_text):
|
||||||
|
''' Parse input and return SQLAlchemy query. '''
|
||||||
|
query = self._search_config.create_query(session)
|
||||||
|
for token in re.split(r'\s+', (query_text or '').lower()):
|
||||||
|
if not token:
|
||||||
|
continue
|
||||||
|
negated = False
|
||||||
|
while token[0] == '-':
|
||||||
|
token = token[1:]
|
||||||
|
negated = not negated
|
||||||
|
|
||||||
|
if ':' in token:
|
||||||
|
key, value = token.split(':', 2)
|
||||||
|
query = self._handle_key_value(query, key, value, negated)
|
||||||
|
else:
|
||||||
|
query = self._handle_anonymous(
|
||||||
|
query, self._create_criterion(token, negated))
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def _handle_key_value(self, query, key, value, negated):
|
||||||
|
if key == 'order':
|
||||||
|
if value.count(',') == 0:
|
||||||
|
order = self.ORDER_ASC
|
||||||
|
elif value.count(',') == 1:
|
||||||
|
value, order_str = value.split(',')
|
||||||
|
if order_str == 'asc':
|
||||||
|
order = self.ORDER_ASC
|
||||||
|
elif order_str == 'desc':
|
||||||
|
order = self.ORDER_DESC
|
||||||
|
else:
|
||||||
|
raise SearchError('Unknown search direction: %r.' % order_str)
|
||||||
|
else:
|
||||||
|
raise SearchError('Too many commas in order search token.')
|
||||||
|
if negated:
|
||||||
|
if order == self.ORDER_DESC:
|
||||||
|
order = self.ORDER_ASC
|
||||||
|
else:
|
||||||
|
order = self.ORDER_DESC
|
||||||
|
return self._handle_order(query, value, order)
|
||||||
|
elif key == 'special':
|
||||||
|
return self._handle_special(query, value, negated)
|
||||||
|
else:
|
||||||
|
return self._handle_named(
|
||||||
|
query, key, self._create_criterion(value, negated))
|
||||||
|
|
||||||
|
def _handle_anonymous(self, query, criterion):
|
||||||
|
if not self._search_config.anonymous_filter:
|
||||||
|
raise SearchError(
|
||||||
|
'Anonymous tokens are not valid in this context.')
|
||||||
|
return self._search_config.anonymous_filter(query, criterion)
|
||||||
|
|
||||||
|
def _handle_named(self, query, key, criterion):
|
||||||
|
if key in self._search_config.named_filters:
|
||||||
|
return self._search_config.named_filters[key](query, criterion)
|
||||||
|
raise SearchError(
|
||||||
|
'Unknown named token: %r. Available named tokens: %r.' % (
|
||||||
|
key, list(self._search_config.named_filters.keys())))
|
||||||
|
|
||||||
|
def _handle_special(self, query, value, negated):
|
||||||
|
if value in self._search_config.special_filters:
|
||||||
|
return self._search_config.special_filters[value](query, criterion)
|
||||||
|
raise SearchError(
|
||||||
|
'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 = self._search_config.order_columns[value]
|
||||||
|
if order == self.ORDER_ASC:
|
||||||
|
column = column.asc()
|
||||||
|
else:
|
||||||
|
column = column.desc()
|
||||||
|
return query.order_by(column)
|
||||||
|
raise SearchError(
|
||||||
|
'Unknown search order: %r. Available search orders: %r.' % (
|
||||||
|
value, list(self._search_config.order_columns.keys())))
|
||||||
|
|
||||||
|
def _create_criterion(self, value, negated):
|
||||||
|
if '..' in value:
|
||||||
|
low, high = value.split('..')
|
||||||
|
if not low and not high:
|
||||||
|
raise SearchError('Empty ranged value')
|
||||||
|
return RangedSearchCriterion(value, negated, low, high)
|
||||||
|
if ',' in value:
|
||||||
|
return ArraySearchCriterion(value, negated, value.split(','))
|
||||||
|
return StringSearchCriterion(value, negated, value)
|
45
server/szurubooru/services/search/user_search_config.py
Normal file
45
server/szurubooru/services/search/user_search_config.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
''' Exports UserSearchConfig. '''
|
||||||
|
|
||||||
|
from sqlalchemy.sql.expression import func
|
||||||
|
from szurubooru.errors import SearchError
|
||||||
|
from szurubooru.model import User
|
||||||
|
from szurubooru.services.search.base_search_config import BaseSearchConfig
|
||||||
|
|
||||||
|
class UserSearchConfig(BaseSearchConfig):
|
||||||
|
''' Executes searches related to the users. '''
|
||||||
|
|
||||||
|
def create_query(self, session):
|
||||||
|
return session.query(User)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def anonymous_filter(self):
|
||||||
|
return self._create_basic_filter(User.name, allow_ranged=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def special_filters(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def named_filters(self):
|
||||||
|
return {
|
||||||
|
'name': self._create_basic_filter(User.name, allow_ranged=False),
|
||||||
|
'creation_date': self._create_date_filter(User.creation_time),
|
||||||
|
'creation_time': self._create_date_filter(User.creation_time),
|
||||||
|
'last_login_date': self._create_date_filter(User.last_login_time),
|
||||||
|
'last_login_time': self._create_date_filter(User.last_login_time),
|
||||||
|
'login_date': self._create_date_filter(User.last_login_time),
|
||||||
|
'login_time': self._create_date_filter(User.last_login_time),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def order_columns(self):
|
||||||
|
return {
|
||||||
|
'random': func.random(),
|
||||||
|
'name': User.name,
|
||||||
|
'creation_date': User.creation_time,
|
||||||
|
'creation_time': User.creation_time,
|
||||||
|
'last_login_date': User.last_login_time,
|
||||||
|
'last_login_time': User.last_login_time,
|
||||||
|
'login_date': User.last_login_time,
|
||||||
|
'login_time': User.last_login_time,
|
||||||
|
}
|
0
server/szurubooru/tests/__init__.py
Normal file
0
server/szurubooru/tests/__init__.py
Normal file
11
server/szurubooru/tests/database_test_case.py
Normal file
11
server/szurubooru/tests/database_test_case.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import unittest
|
||||||
|
import sqlalchemy
|
||||||
|
from szurubooru.model import Base
|
||||||
|
|
||||||
|
class DatabaseTestCase(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
engine = sqlalchemy.create_engine('sqlite:///:memory:')
|
||||||
|
session_maker = sqlalchemy.orm.sessionmaker(bind=engine)
|
||||||
|
self.session = sqlalchemy.orm.scoped_session(session_maker)
|
||||||
|
Base.query = self.session.query_property()
|
||||||
|
Base.metadata.create_all(bind=engine)
|
0
server/szurubooru/tests/services/__init__.py
Normal file
0
server/szurubooru/tests/services/__init__.py
Normal file
0
server/szurubooru/tests/services/search/__init__.py
Normal file
0
server/szurubooru/tests/services/search/__init__.py
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from szurubooru.errors import SearchError
|
||||||
|
from szurubooru.model.user import User
|
||||||
|
from szurubooru.services.search.search_executor import SearchExecutor
|
||||||
|
from szurubooru.services.search.user_search_config import UserSearchConfig
|
||||||
|
from szurubooru.tests.database_test_case import DatabaseTestCase
|
||||||
|
|
||||||
|
class TestUserSearchExecutor(DatabaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.search_config = UserSearchConfig()
|
||||||
|
self.executor = SearchExecutor(self.search_config)
|
||||||
|
|
||||||
|
def _create_user(self, name):
|
||||||
|
user = User()
|
||||||
|
user.name = name
|
||||||
|
user.password = 'dummy'
|
||||||
|
user.password_salt = 'dummy'
|
||||||
|
user.password_hash = 'dummy'
|
||||||
|
user.email = 'dummy'
|
||||||
|
user.access_rank = 'dummy'
|
||||||
|
user.creation_time = datetime.now()
|
||||||
|
user.avatar_style = User.AVATAR_GRAVATAR
|
||||||
|
return user
|
||||||
|
|
||||||
|
def _test(self, query, page, expected_count, expected_user_names):
|
||||||
|
count, users = self.executor.execute(self.session, query, page)
|
||||||
|
self.assertEqual(count, expected_count)
|
||||||
|
self.assertEqual([u.name for u in users], expected_user_names)
|
||||||
|
|
||||||
|
def test_filter_by_creation_time(self):
|
||||||
|
user1 = self._create_user('u1')
|
||||||
|
user2 = self._create_user('u2')
|
||||||
|
user1.creation_time = datetime(2014, 1, 1)
|
||||||
|
user2.creation_time = datetime(2015, 1, 1)
|
||||||
|
self.session.add_all([user1, user2])
|
||||||
|
for alias in ['creation_time', 'creation_date']:
|
||||||
|
self._test('%s:2014' % alias, 1, 1, ['u1'])
|
||||||
|
|
||||||
|
def test_filter_by_negated_creation_time(self):
|
||||||
|
user1 = self._create_user('u1')
|
||||||
|
user2 = self._create_user('u2')
|
||||||
|
user1.creation_time = datetime(2014, 1, 1)
|
||||||
|
user2.creation_time = datetime(2015, 1, 1)
|
||||||
|
self.session.add_all([user1, user2])
|
||||||
|
for alias in ['creation_time', 'creation_date']:
|
||||||
|
self._test('-%s:2014' % alias, 1, 1, ['u2'])
|
||||||
|
|
||||||
|
def test_filter_by_ranged_creation_time(self):
|
||||||
|
user1 = self._create_user('u1')
|
||||||
|
user2 = self._create_user('u2')
|
||||||
|
user3 = self._create_user('u3')
|
||||||
|
user1.creation_time = datetime(2014, 1, 1)
|
||||||
|
user2.creation_time = datetime(2014, 6, 1)
|
||||||
|
user3.creation_time = datetime(2015, 1, 1)
|
||||||
|
self.session.add_all([user1, user2, user3])
|
||||||
|
for alias in ['creation_time', 'creation_date']:
|
||||||
|
self._test('%s:2014..2014-06' % alias, 1, 2, ['u1', 'u2'])
|
||||||
|
self._test('%s:2014-06..2015-01-01' % alias, 1, 2, ['u2', 'u3'])
|
||||||
|
self._test('%s:2014-06..' % alias, 1, 2, ['u2', 'u3'])
|
||||||
|
self._test('%s:..2014-06' % alias, 1, 2, ['u1', 'u2'])
|
||||||
|
self.assertRaises(
|
||||||
|
SearchError, self.executor.execute, self.session, '%s:..', 1)
|
||||||
|
|
||||||
|
def test_filter_by_negated_ranged_creation_time(self):
|
||||||
|
user1 = self._create_user('u1')
|
||||||
|
user2 = self._create_user('u2')
|
||||||
|
user3 = self._create_user('u3')
|
||||||
|
user1.creation_time = datetime(2014, 1, 1)
|
||||||
|
user2.creation_time = datetime(2014, 6, 1)
|
||||||
|
user3.creation_time = datetime(2015, 1, 1)
|
||||||
|
self.session.add_all([user1, user2, user3])
|
||||||
|
for alias in ['creation_time', 'creation_date']:
|
||||||
|
self._test('-%s:2014..2014-06' % alias, 1, 1, ['u3'])
|
||||||
|
self._test('-%s:2014-06..2015-01-01' % alias, 1, 1, ['u1'])
|
||||||
|
|
||||||
|
def test_filter_by_composite_creation_time(self):
|
||||||
|
user1 = self._create_user('u1')
|
||||||
|
user2 = self._create_user('u2')
|
||||||
|
user3 = self._create_user('u3')
|
||||||
|
user1.creation_time = datetime(2014, 1, 1)
|
||||||
|
user2.creation_time = datetime(2014, 6, 1)
|
||||||
|
user3.creation_time = datetime(2015, 1, 1)
|
||||||
|
self.session.add_all([user1, user2, user3])
|
||||||
|
for alias in ['creation_time', 'creation_date']:
|
||||||
|
self._test('%s:2014-01,2015' % alias, 1, 2, ['u1', 'u3'])
|
||||||
|
|
||||||
|
def test_filter_by_negated_composite_creation_time(self):
|
||||||
|
user1 = self._create_user('u1')
|
||||||
|
user2 = self._create_user('u2')
|
||||||
|
user3 = self._create_user('u3')
|
||||||
|
user1.creation_time = datetime(2014, 1, 1)
|
||||||
|
user2.creation_time = datetime(2014, 6, 1)
|
||||||
|
user3.creation_time = datetime(2015, 1, 1)
|
||||||
|
self.session.add_all([user1, user2, user3])
|
||||||
|
for alias in ['creation_time', 'creation_date']:
|
||||||
|
self._test('-%s:2014-01,2015' % alias, 1, 1, ['u2'])
|
||||||
|
|
||||||
|
def test_filter_by_name(self):
|
||||||
|
self.session.add(self._create_user('u1'))
|
||||||
|
self.session.add(self._create_user('u2'))
|
||||||
|
self._test('name:u1', 1, 1, ['u1'])
|
||||||
|
self._test('name:u2', 1, 1, ['u2'])
|
||||||
|
|
||||||
|
def test_filter_by_negated_name(self):
|
||||||
|
self.session.add(self._create_user('u1'))
|
||||||
|
self.session.add(self._create_user('u2'))
|
||||||
|
self._test('-name:u1', 1, 1, ['u2'])
|
||||||
|
self._test('-name:u2', 1, 1, ['u1'])
|
||||||
|
|
||||||
|
def test_filter_by_composite_name(self):
|
||||||
|
self.session.add(self._create_user('u1'))
|
||||||
|
self.session.add(self._create_user('u2'))
|
||||||
|
self.session.add(self._create_user('u3'))
|
||||||
|
self._test('name:u1,u2', 1, 2, ['u1', 'u2'])
|
||||||
|
|
||||||
|
def test_filter_by_ranged_name(self):
|
||||||
|
self.assertRaises(
|
||||||
|
SearchError, self.executor.execute, self.session, 'name:u1..u2', 1)
|
||||||
|
|
||||||
|
def test_paging(self):
|
||||||
|
self.executor.page_size = 1
|
||||||
|
self.session.add(self._create_user('u1'))
|
||||||
|
self.session.add(self._create_user('u2'))
|
||||||
|
self._test('', 1, 2, ['u1'])
|
||||||
|
self._test('', 2, 2, ['u2'])
|
||||||
|
|
||||||
|
def test_order_by_name(self):
|
||||||
|
self.session.add(self._create_user('u1'))
|
||||||
|
self.session.add(self._create_user('u2'))
|
||||||
|
self._test('order:name', 1, 2, ['u1', 'u2'])
|
||||||
|
self._test('-order:name', 1, 2, ['u2', 'u1'])
|
||||||
|
self._test('order:name,asc', 1, 2, ['u1', 'u2'])
|
||||||
|
self._test('order:name,desc', 1, 2, ['u2', 'u1'])
|
||||||
|
self._test('-order:name,asc', 1, 2, ['u2', 'u1'])
|
||||||
|
self._test('-order:name,desc', 1, 2, ['u1', 'u2'])
|
||||||
|
|
||||||
|
def test_anonymous(self):
|
||||||
|
self.session.add(self._create_user('u1'))
|
||||||
|
self.session.add(self._create_user('u2'))
|
||||||
|
self._test('u1', 1, 1, ['u1'])
|
||||||
|
self._test('u2', 1, 1, ['u2'])
|
||||||
|
|
||||||
|
def test_negated_anonymous(self):
|
||||||
|
self.session.add(self._create_user('u1'))
|
||||||
|
self.session.add(self._create_user('u2'))
|
||||||
|
self._test('-u1', 1, 1, ['u2'])
|
||||||
|
self._test('-u2', 1, 1, ['u1'])
|
||||||
|
|
||||||
|
def test_combining(self):
|
||||||
|
user1 = self._create_user('u1')
|
||||||
|
user2 = self._create_user('u2')
|
||||||
|
user3 = self._create_user('u3')
|
||||||
|
user1.creation_time = datetime(2014, 1, 1)
|
||||||
|
user2.creation_time = datetime(2014, 6, 1)
|
||||||
|
user3.creation_time = datetime(2015, 1, 1)
|
||||||
|
self.session.add_all([user1, user2, user3])
|
||||||
|
self._test('creation_time:2014 u1', 1, 1, ['u1'])
|
||||||
|
self._test('creation_time:2014 u2', 1, 1, ['u2'])
|
||||||
|
self._test('creation_time:2016 u2', 1, 0, [])
|
||||||
|
|
||||||
|
def test_special(self):
|
||||||
|
self.assertRaises(
|
||||||
|
SearchError, self.executor.execute, self.session, 'special:-', 1)
|
Loading…
Reference in a new issue