server/tags: add tag listing
This commit is contained in:
parent
c71c082000
commit
61d2fb88ea
10 changed files with 664 additions and 130 deletions
95
API.md
95
API.md
|
@ -83,7 +83,82 @@ data.
|
|||
|
||||
|
||||
## Listing tags
|
||||
Not yet implemented.
|
||||
- **Request**
|
||||
|
||||
`GET /tags/?page=<page>&pageSize=<page-size>&query=<query>`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"query": "haruhi",
|
||||
"tags": [
|
||||
<tag>,
|
||||
<tag>,
|
||||
<tag>,
|
||||
<tag>,
|
||||
<tag>
|
||||
],
|
||||
"page": 1,
|
||||
"pageSize": 5,
|
||||
"total": 7
|
||||
}
|
||||
```
|
||||
...where `<tag>` is a [tag resource](#tag) and `query` contains standard
|
||||
[search query](#search).
|
||||
|
||||
- **Errors**
|
||||
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Searches for tags.
|
||||
|
||||
**Anonymous tokens**
|
||||
|
||||
Same as `name` token.
|
||||
|
||||
**Named tokens**
|
||||
|
||||
| `<value>` | Description |
|
||||
| ------------------- | ------------------------------------- |
|
||||
| `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**
|
||||
|
||||
| `<value>` | Description |
|
||||
| ------------------- | ---------------------------- |
|
||||
| `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.
|
||||
|
||||
|
||||
## Creating tag
|
||||
|
@ -264,15 +339,15 @@ Not yet implemented.
|
|||
|
||||
**Named tokens**
|
||||
|
||||
| `<value>` | Description |
|
||||
| ----------------- | ------------------------------------------------ |
|
||||
| `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 |
|
||||
| `last-login-time` | alias of `last-login-date` |
|
||||
| `login-date` | alias of `last-login-date` |
|
||||
| `login-time` | alias of `last-login-date` |
|
||||
| `<value>` | Description |
|
||||
| ----------------- | ----------------------------------------------- |
|
||||
| `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 |
|
||||
| `last-login-time` | alias of `last-login-date` |
|
||||
| `login-date` | alias of `last-login-date` |
|
||||
| `login-time` | alias of `last-login-date` |
|
||||
|
||||
**Order tokens**
|
||||
|
||||
|
|
|
@ -345,3 +345,135 @@ most, uploaded by user Pirate.</p>
|
|||
<p><strong>Special tokens</strong></p>
|
||||
|
||||
<p>None.</p>
|
||||
|
||||
<h1 id='tag-search-help'>Tag search tokens</h1>
|
||||
|
||||
<p><strong>Anonymous tokens</strong></p>
|
||||
|
||||
<p>Same as <code>name</code> token.</p>
|
||||
|
||||
<p><strong>Named tokens</strong></p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>name</code></td>
|
||||
<td>having given name (accepts wildcards)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>category</code></td>
|
||||
<td>having given category</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-date</code></td>
|
||||
<td>created at given date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-time</code></td>
|
||||
<td>alias of <code>creation-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-date</code></td>
|
||||
<td>edited at given date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-time</code></td>
|
||||
<td>alias of <code>last-edit-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-date</code></td>
|
||||
<td>alias of <code>last-edit-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-time</code></td>
|
||||
<td>alias of <code>last-edit-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>usages</code></td>
|
||||
<td>used in given number of posts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>usage-count</code></td>
|
||||
<td>alias of <code>usages</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>post-count</code></td>
|
||||
<td>alias of <code>usages</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>suggestion-count</code></td>
|
||||
<td>with given number of suggestions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>implication-count</code></td>
|
||||
<td>with given number of implications</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>Order tokens</strong></p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>random</code></td>
|
||||
<td>as random as it can get</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>name</code></td>
|
||||
<td>A to Z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>category</code></td>
|
||||
<td>category (A to Z)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-date</code></td>
|
||||
<td>recently created first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-time</code></td>
|
||||
<td>alias of <code>creation-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-date</code></td>
|
||||
<td>recently edited first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-time</code></td>
|
||||
<td>alias of <code>creation-time</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-date</code></td>
|
||||
<td>alias of <code>creation-time</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-time</code></td>
|
||||
<td>alias of <code>creation-time</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>usages</code></td>
|
||||
<td>used in most posts first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>usage-count</code></td>
|
||||
<td>alias of <code>usages</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>post-count</code></td>
|
||||
<td>alias of <code>usages</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>suggestion-count</code></td>
|
||||
<td>with most suggestions first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>implication-count</code></td>
|
||||
<td>with most implications first</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>Special tokens</strong></p>
|
||||
|
||||
<p>None.</p>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
97
server/szurubooru/search/tag_search_config.py
Normal file
97
server/szurubooru/search/tag_search_config.py
Normal file
|
@ -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()
|
|
@ -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),
|
||||
|
|
176
server/szurubooru/tests/search/test_tag_search_config.py
Normal file
176
server/szurubooru/tests/search/test_tag_search_config.py
Normal file
|
@ -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', [])
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue