server/posts: add post listing

This commit is contained in:
rr- 2016-05-07 21:42:03 +02:00
parent 9b591c3f1b
commit 58964bcdc9
12 changed files with 1154 additions and 60 deletions

117
API.md
View file

@ -28,7 +28,7 @@
- [Merging tags](#merging-tags) - [Merging tags](#merging-tags)
- [Listing tag siblings](#listing-tag-siblings) - [Listing tag siblings](#listing-tag-siblings)
- Posts - Posts
- ~~Listing posts~~ - [Listing posts](#listing-posts)
- [Creating post](#creating-post) - [Creating post](#creating-post)
- [Updating post](#updating-post) - [Updating post](#updating-post)
- [Getting post](#getting-post) - [Getting post](#getting-post)
@ -290,7 +290,7 @@ data.
**Named tokens** **Named tokens**
| `<value>` | Description | | `<key>` | Description |
| ------------------- | ------------------------------------- | | ------------------- | ------------------------------------- |
| `name` | having given name (accepts wildcards) | | `name` | having given name (accepts wildcards) |
| `category` | having given category | | `category` | having given category |
@ -516,6 +516,111 @@ data.
appears with given tag. Results are sorted by occurrences count and the appears with given tag. Results are sorted by occurrences count and the
list is truncated to the first 50 elements. Doesn't use paging. list is truncated to the first 50 elements. Doesn't use paging.
## Listing posts
- **Request**
`GET /posts/?page=<page>&pageSize=<page-size>&query=<query>`
- **Output**
A [paged search result resource](#paged-search-result), for which
`<resource>` is a [post resource](#post).
- **Errors**
- privileges are too low
- **Description**
Searches for posts.
**Anonymous tokens**
Same as `tag` token.
**Named tokens**
| `<key>` | Description |
| ---------------- | ---------------------------------------------------------- |
| `id` | having given post number |
| `tag` | having given tag |
| `score` | having given score |
| `uploader` | uploaded by given user |
| `upload` | alias of upload |
| `submit` | alias of upload |
| `comment` | commented by given user |
| `fav` | favorited by given user |
| `tag-count` | having given number of tags |
| `comment-count` | having given number of comments |
| `fav-count` | favorited by given number of users |
| `note-count` | having given number of annotations |
| `feature-count` | having been featured given number of times |
| `type` | given type of posts. `<value>` can be either `image`, `animation` (or `animated` or `anim`), `flash` (or `swf`) or `video` (or `webm`). |
| `file-size` | having given file size (in bytes) |
| `image-width` | having given image width (where applicable) |
| `image-height` | having given image height (where applicable) |
| `image-area` | having given number of pixels (image width * image height) |
| `width` | alias of `image-width` |
| `height` | alias of `image-height` |
| `area` | alias of `image-area` |
| `creation-date` | posted at given date |
| `creation-time` | alias of `creation-date` |
| `date` | alias of `creation-date` |
| `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` |
| `comment-date` | commented at given date |
| `comment-time` | alias of `comment-date` |
| `fav-date` | last favorited at given date |
| `fav-time` | alias of `fav-date` |
| `feature-date` | featured at given date |
| `feature-time` | alias of `feature-time` |
**Sort style tokens**
| `<value>` | Description |
| ---------------- | ------------------------------------------------ |
| `random` | as random as it can get |
| `id` | highest to lowest post number |
| `score` | highest scored |
| `tag-count` | with most tags |
| `comment-count` | most commented first |
| `fav-count` | loved by most |
| `note-count` | with most annotations |
| `feature-count` | most often featured |
| `file-size` | largest files first |
| `image-width` | widest images first |
| `image-height` | tallest images first |
| `image-area` | largest images first |
| `width` | alias of `image-width` |
| `height` | alias of `image-height` |
| `area` | alias of `image-area` |
| `creation-date` | newest to oldest (pretty much same as id) |
| `creation-time` | alias of `creation-date` |
| `date` | alias of `creation-date` |
| `time` | alias of `creation-date` |
| `last-edit-date` | like creation-date, only looks at last edit time |
| `last-edit-time` | alias of `last-edit-date` |
| `edit-date` | alias of `last-edit-date` |
| `edit-time` | alias of `last-edit-date` |
| `comment-date` | recently commented by anyone |
| `comment-time` | alias of `comment-date` |
| `fav-date` | recently added to favorites by anyone |
| `fav-time` | alias of `fav-date` |
| `feature-date` | recently featured |
| `feature-time` | alias of `feature-time` |
**Special tokens**
| `<value>` | Description |
| ------------ | ------------------------------------------------------------- |
| `liked` | posts liked by currently logged in user |
| `disliked` | posts disliked by currently logged in user |
| `fav` | posts added to favorites by currently logged in user |
| `tumbleweed` | posts with score of 0, without comments and without favorites |
## Creating post ## Creating post
- **Request** - **Request**
@ -770,11 +875,12 @@ data.
**Named tokens** **Named tokens**
| `<value>` | Description | | `<key>` | Description |
| ---------------- | ---------------------------------------------- | | ---------------- | ---------------------------------------------- |
| `id` | specific comment ID | | `id` | specific comment ID |
| `post` | specific post ID | | `post` | specific post ID |
| `user` | created by given user (accepts wildcards) | | `user` | created by given user (accepts wildcards) |
| `author` | alias of `user` |
| `text` | containing given text (accepts wildcards) | | `text` | containing given text (accepts wildcards) |
| `creation-date` | created at given date | | `creation-date` | created at given date |
| `creation-time` | alias of `creation-date` | | `creation-time` | alias of `creation-date` |
@ -789,6 +895,7 @@ data.
| ---------------- | ------------------------- | | ---------------- | ------------------------- |
| `random` | as random as it can get | | `random` | as random as it can get |
| `user` | author name, A to Z | | `user` | author name, A to Z |
| `author` | alias of `user` |
| `post` | post ID, newest to oldest | | `post` | post ID, newest to oldest |
| `creation-date` | newest to oldest | | `creation-date` | newest to oldest |
| `creation-time` | alias of `creation-date` | | `creation-time` | alias of `creation-date` |
@ -946,7 +1053,7 @@ data.
**Named tokens** **Named tokens**
| `<value>` | Description | | `<key>` | Description |
| ----------------- | ----------------------------------------------- | | ----------------- | ----------------------------------------------- |
| `name` | having given name (accepts wildcards) | | `name` | having given name (accepts wildcards) |
| `creation-date` | registered at given date | | `creation-date` | registered at given date |
@ -1182,7 +1289,7 @@ data.
**Named tokens** **Named tokens**
| `<value>` | Description | | `<key>` | Description |
| ----------------- | --------------------------------------------- | | ----------------- | --------------------------------------------- |
| `type` | involving given resource type | | `type` | involving given resource type |
| `id` | involving given resource id | | `id` | involving given resource id |

View file

@ -1,6 +1,6 @@
<p><strong>Anonymous tokens</strong></p> <p><strong>Anonymous tokens</strong></p>
<p>Filter posts tagged with given <code>&lt;value&gt;</code>.</p> <p>Same as <code>tag</code> token.</p>
<p><strong>Named tokens</strong></p> <p><strong>Named tokens</strong></p>
@ -8,7 +8,11 @@
<tbody> <tbody>
<tr> <tr>
<td><code>id</code></td> <td><code>id</code></td>
<td>having specific post ID</td> <td>having given post number</td>
</tr>
<tr>
<td><code>tag</code></td>
<td>having given tag</td>
</tr> </tr>
<tr> <tr>
<td><code>score</code></td> <td><code>score</code></td>
@ -18,6 +22,14 @@
<td><code>uploader</code></td> <td><code>uploader</code></td>
<td>uploaded by given user</td> <td>uploaded by given user</td>
</tr> </tr>
<tr>
<td><code>upload</code></td>
<td>alias of <code>upload</code></td>
</tr>
<tr>
<td><code>submit</code></td>
<td>alias of <code>upload</code></td>
</tr>
<tr> <tr>
<td><code>comment</code></td> <td><code>comment</code></td>
<td>commented by given user</td> <td>commented by given user</td>
@ -27,16 +39,16 @@
<td>favorited by given user</td> <td>favorited by given user</td>
</tr> </tr>
<tr> <tr>
<td><code>fav-count</code></td> <td><code>tag-count</code></td>
<td>favorited by given number of users</td> <td>having given number of tags</td>
</tr> </tr>
<tr> <tr>
<td><code>comment-count</code></td> <td><code>comment-count</code></td>
<td>having given number of comments</td> <td>having given number of comments</td>
</tr> </tr>
<tr> <tr>
<td><code>tag-count</code></td> <td><code>fav-count</code></td>
<td>having given number of tags</td> <td>favorited by given number of users</td>
</tr> </tr>
<tr> <tr>
<td><code>note-count</code></td> <td><code>note-count</code></td>
@ -47,8 +59,8 @@
<td>having been featured given number of times</td> <td>having been featured given number of times</td>
</tr> </tr>
<tr> <tr>
<td><code>date</code></td> <td><code>type</code></td>
<td>posted at given date</td> <td>given type of posts. <code>&lt;value&gt;</code> can be either <code>image</code>, <code>animation</code> (or <code>animated</code> or <code>anim</code>), <code>flash</code> (or <code>swf</code>) or <code>video</code> (or <code>webm</code>).</td>
</tr> </tr>
<tr> <tr>
<td><code>file-size</code></td> <td><code>file-size</code></td>
@ -67,8 +79,72 @@
<td>having given number of pixels (image width * image height)</td> <td>having given number of pixels (image width * image height)</td>
</tr> </tr>
<tr> <tr>
<td><code>type</code></td> <td><code>width</code></td>
<td>given type of posts (<code>&lt;value&gt;</code> can be either <code>image</code>, <code>flash</code>/<code>swf</code>, <code>youtube</code>/<code>yt</code>, <code>video</code> or <code>animation</code>)</td> <td>alias of <code>image-width</code></td>
</tr>
<tr>
<td><code>height</code></td>
<td>alias of <code>image-height</code></td>
</tr>
<tr>
<td><code>area</code></td>
<td>alias of <code>image-area</code></td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>posted at given date</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>date</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>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>comment-date</code></td>
<td>commented at given date</td>
</tr>
<tr>
<td><code>comment-time</code></td>
<td>alias of <code>comment-date</code></td>
</tr>
<tr>
<td><code>fav-date</code></td>
<td>last favorited at given date</td>
</tr>
<tr>
<td><code>fav-time</code></td>
<td>alias of <code>fav-date</code></td>
</tr>
<tr>
<td><code>feature-date</code></td>
<td>featured at given date</td>
</tr>
<tr>
<td><code>feature-time</code></td>
<td>alias of <code>feature-time</code></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -83,28 +159,32 @@
</tr> </tr>
<tr> <tr>
<td><code>id</code></td> <td><code>id</code></td>
<td>highest to lowest post ID</td> <td>highest to lowest post number</td>
</tr> </tr>
<tr> <tr>
<td><code>score</code></td> <td><code>score</code></td>
<td>highest scored</td> <td>highest scored</td>
</tr> </tr>
<tr> <tr>
<td><code>fav-count</code></td> <td><code>tag-count</code></td>
<td>loved by most</td> <td>with most tags</td>
</tr> </tr>
<tr> <tr>
<td><code>comment-count</code></td> <td><code>comment-count</code></td>
<td>most commented first</td> <td>most commented first</td>
</tr> </tr>
<tr> <tr>
<td><code>tag-count</code></td> <td><code>fav-count</code></td>
<td>with most tags</td> <td>loved by most</td>
</tr> </tr>
<tr> <tr>
<td><code>note-count</code></td> <td><code>note-count</code></td>
<td>with most annotations</td> <td>with most annotations</td>
</tr> </tr>
<tr>
<td><code>feature-count</code></td>
<td>most often featured</td>
</tr>
<tr> <tr>
<td><code>file-size</code></td> <td><code>file-size</code></td>
<td>largest files first</td> <td>largest files first</td>
@ -121,29 +201,73 @@
<td><code>image-area</code></td> <td><code>image-area</code></td>
<td>largest images first</td> <td>largest images first</td>
</tr> </tr>
<tr>
<td><code>width</code></td>
<td>alias of <code>image-width</code></td>
</tr>
<tr>
<td><code>height</code></td>
<td>alias of <code>image-height</code></td>
</tr>
<tr>
<td><code>area</code></td>
<td>alias of <code>image-area</code></td>
</tr>
<tr> <tr>
<td><code>creation-date</code></td> <td><code>creation-date</code></td>
<td>newest to oldest (pretty much same as <code>id</code>)</td> <td>newest to oldest (pretty much same as <code>id</code>)</td>
</tr> </tr>
<tr> <tr>
<td><code>edit-date</code></td> <td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>date</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>like <code>creation-date</code>, only looks at last edit time</td> <td>like <code>creation-date</code>, only looks at last edit time</td>
</tr> </tr>
<tr> <tr>
<td><code>fav-date</code></td> <td><code>last-edit-time</code></td>
<td>recently added to favorites by anyone</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>
<tr> <tr>
<td><code>comment-date</code></td> <td><code>comment-date</code></td>
<td>recently commented by anyone</td> <td>recently commented by anyone</td>
</tr> </tr>
<tr>
<td><code>comment-time</code></td>
<td>alias of <code>comment-date</code></td>
</tr>
<tr>
<td><code>fav-date</code></td>
<td>recently added to favorites by anyone</td>
</tr>
<tr>
<td><code>fav-time</code></td>
<td>alias of <code>fav-date</code></td>
</tr>
<tr> <tr>
<td><code>feature-date</code></td> <td><code>feature-date</code></td>
<td>recently featured</td> <td>recently featured</td>
</tr> </tr>
<tr> <tr>
<td><code>feature-count</code></td> <td><code>feature-time</code></td>
<td>most often featured</td> <td>alias of <code>feature-time</code></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -1,8 +1,19 @@
import datetime import datetime
from szurubooru import search
from szurubooru.api.base_api import BaseApi from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, tags, posts, snapshots, favorites, scores from szurubooru.func import auth, tags, posts, snapshots, favorites, scores
class PostListApi(BaseApi): class PostListApi(BaseApi):
def __init__(self):
super().__init__()
self._search_executor = search.SearchExecutor(search.PostSearchConfig())
def get(self, ctx):
auth.verify_privilege(ctx.user, 'posts:list')
self._search_executor.config.user = ctx.user
return self._search_executor.execute_and_serialize(
ctx, lambda post: posts.serialize_post(post, ctx.user))
def post(self, ctx): def post(self, ctx):
auth.verify_privilege(ctx.user, 'posts:create') auth.verify_privilege(ctx.user, 'posts:create')
content = ctx.get_file('content', required=True) content = ctx.get_file('content', required=True)

View file

@ -2,6 +2,7 @@ from sqlalchemy import Column, Integer, DateTime, String, Text, PickleType, Fore
from sqlalchemy.orm import relationship, column_property, object_session from sqlalchemy.orm import relationship, column_property, object_session
from sqlalchemy.sql.expression import func, select from sqlalchemy.sql.expression import func, select
from szurubooru.db.base import Base from szurubooru.db.base import Base
from szurubooru.db.comment import Comment
class PostFeature(Base): class PostFeature(Base):
__tablename__ = 'post_feature' __tablename__ = 'post_feature'
@ -117,6 +118,8 @@ class Post(Base):
.where(PostTag.post_id == post_id) \ .where(PostTag.post_id == post_id) \
.correlate_except(PostTag)) .correlate_except(PostTag))
canvas_area = column_property(canvas_width * canvas_height)
@property @property
def is_featured(self): def is_featured(self):
featured_post = object_session(self) \ featured_post = object_session(self) \
@ -125,12 +128,10 @@ class Post(Base):
.first() .first()
return featured_post and featured_post.post_id == self.post_id return featured_post and featured_post.post_id == self.post_id
@property score = column_property(
def score(self): select([func.coalesce(func.sum(PostScore.score), 0)]) \
return object_session(self) \ .where(PostScore.post_id == post_id) \
.query(func.sum(PostScore.score)) \ .correlate_except(PostScore))
.filter(PostScore.post_id == self.post_id) \
.one()[0] or 0
favorite_count = column_property( favorite_count = column_property(
select([func.count(PostFavorite.post_id)]) \ select([func.count(PostFavorite.post_id)]) \
@ -152,10 +153,22 @@ class Post(Base):
.where(PostFeature.post_id == post_id) \ .where(PostFeature.post_id == post_id) \
.correlate_except(PostFeature)) .correlate_except(PostFeature))
# TODO: wire these comment_count = column_property(
#comment_count = Column('auto_comment_count', Integer, nullable=False, default=0) select([func.count(Comment.post_id)]) \
#note_count = Column('auto_note_count', Integer, nullable=False, default=0) .where(Comment.post_id == post_id) \
#last_comment_edit_time = Column( .correlate_except(Comment))
# 'auto_comment_creation_time', Integer, nullable=False, default=0)
#last_comment_creation_time = Column( last_comment_creation_time = column_property(
# 'auto_comment_edit_time', Integer, nullable=False, default=0) select([func.max(Comment.creation_time)]) \
.where(Comment.post_id == post_id) \
.correlate_except(Comment))
last_comment_edit_time = column_property(
select([func.max(Comment.last_edit_time)]) \
.where(Comment.post_id == post_id) \
.correlate_except(Comment))
note_count = column_property(
select([func.count(PostNote.post_id)]) \
.where(PostNote.post_id == post_id) \
.correlate_except(PostNote))

View file

@ -4,6 +4,15 @@ import re
from sqlalchemy.inspection import inspect from sqlalchemy.inspection import inspect
from szurubooru.errors import ValidationError from szurubooru.errors import ValidationError
def unalias_dict(input_dict):
output_dict = {}
for key_list, value in input_dict.items():
if isinstance(key_list, str):
key_list = [key_list]
for key in key_list:
output_dict[key] = value
return output_dict
def get_md5(source): def get_md5(source):
if not isinstance(source, bytes): if not isinstance(source, bytes):
source = source.encode('utf-8') source = source.encode('utf-8')

View file

@ -5,3 +5,4 @@ from szurubooru.search.user_search_config import UserSearchConfig
from szurubooru.search.snapshot_search_config import SnapshotSearchConfig from szurubooru.search.snapshot_search_config import SnapshotSearchConfig
from szurubooru.search.tag_search_config import TagSearchConfig from szurubooru.search.tag_search_config import TagSearchConfig
from szurubooru.search.comment_search_config import CommentSearchConfig from szurubooru.search.comment_search_config import CommentSearchConfig
from szurubooru.search.post_search_config import PostSearchConfig

View file

@ -1,8 +1,12 @@
import sqlalchemy import sqlalchemy
import szurubooru.errors import szurubooru.errors
from szurubooru import db
from szurubooru.func import util from szurubooru.func import util
from szurubooru.search import criteria from szurubooru.search import criteria
def wildcard_transformer(value):
return value.replace('*', '%')
class BaseSearchConfig(object): class BaseSearchConfig(object):
SORT_DESC = -1 SORT_DESC = -1
SORT_ASC = 1 SORT_ASC = 1
@ -50,17 +54,16 @@ class BaseSearchConfig(object):
BaseSearchConfig._apply_num_criterion_to_column(column, criterion)) BaseSearchConfig._apply_num_criterion_to_column(column, criterion))
@staticmethod @staticmethod
def _apply_str_criterion_to_column(column, criterion): def _apply_str_criterion_to_column(column, criterion, transformer):
''' '''
Decorate SQLAlchemy filter on given column using supplied criterion. Decorate SQLAlchemy filter on given column using supplied criterion.
Parse potential wildcards inside the criterion.
''' '''
if isinstance(criterion, criteria.PlainSearchCriterion): if isinstance(criterion, criteria.PlainSearchCriterion):
expr = column.like(criterion.value.replace('*', '%')) expr = column.like(transformer(criterion.value))
elif isinstance(criterion, criteria.ArraySearchCriterion): elif isinstance(criterion, criteria.ArraySearchCriterion):
expr = sqlalchemy.sql.false() expr = sqlalchemy.sql.false()
for value in criterion.values: for value in criterion.values:
expr = expr | column.like(value.replace('*', '%')) expr = expr | column.like(transformer(value))
elif isinstance(criterion, criteria.RangedSearchCriterion): elif isinstance(criterion, criteria.RangedSearchCriterion):
raise szurubooru.errors.SearchError( raise szurubooru.errors.SearchError(
'Composite token %r is invalid in this context.' % (criterion,)) 'Composite token %r is invalid in this context.' % (criterion,))
@ -71,9 +74,10 @@ class BaseSearchConfig(object):
return expr return expr
@staticmethod @staticmethod
def _create_str_filter(column): def _create_str_filter(column, transformer=wildcard_transformer):
return lambda query, criterion: query.filter( return lambda query, criterion: query.filter(
BaseSearchConfig._apply_str_criterion_to_column(column, criterion)) BaseSearchConfig._apply_str_criterion_to_column(
column, criterion, transformer))
@staticmethod @staticmethod
def _apply_date_criterion_to_column(column, criterion): def _apply_date_criterion_to_column(column, criterion):
@ -111,3 +115,21 @@ class BaseSearchConfig(object):
def _create_date_filter(column): def _create_date_filter(column):
return lambda query, criterion: query.filter( return lambda query, criterion: query.filter(
BaseSearchConfig._apply_date_criterion_to_column(column, criterion)) BaseSearchConfig._apply_date_criterion_to_column(column, criterion))
@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 func(query, criterion):
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)
subquery = subquery.subquery('t')
return query.filter(left_id_column == subquery.c.foreign_id)
return func

View file

@ -19,6 +19,7 @@ class CommentSearchConfig(BaseSearchConfig):
'id': self._create_num_filter(db.Comment.comment_id), 'id': self._create_num_filter(db.Comment.comment_id),
'post': self._create_num_filter(db.Comment.post_id), 'post': self._create_num_filter(db.Comment.post_id),
'user': self._create_str_filter(db.User.name), 'user': self._create_str_filter(db.User.name),
'author': self._create_str_filter(db.User.name),
'text': self._create_str_filter(db.Comment.text), 'text': self._create_str_filter(db.Comment.text),
'creation-date': self._create_date_filter(db.Comment.creation_time), 'creation-date': self._create_date_filter(db.Comment.creation_time),
'creation-time': self._create_date_filter(db.Comment.creation_time), 'creation-time': self._create_date_filter(db.Comment.creation_time),
@ -33,6 +34,7 @@ class CommentSearchConfig(BaseSearchConfig):
return { return {
'random': (func.random(), None), 'random': (func.random(), None),
'user': (db.User.name, self.SORT_ASC), 'user': (db.User.name, self.SORT_ASC),
'author': (db.User.name, self.SORT_ASC),
'post': (db.Comment.post_id, self.SORT_DESC), 'post': (db.Comment.post_id, self.SORT_DESC),
'creation-date': (db.Comment.creation_time, self.SORT_DESC), 'creation-date': (db.Comment.creation_time, self.SORT_DESC),
'creation-time': (db.Comment.creation_time, self.SORT_DESC), 'creation-time': (db.Comment.creation_time, self.SORT_DESC),

View file

@ -0,0 +1,175 @@
from sqlalchemy.sql.expression import func
from szurubooru import db, errors
from szurubooru.func import util
from szurubooru.search.base_search_config import BaseSearchConfig
def _type_transformer(value):
available_types = {
'image': db.Post.TYPE_IMAGE,
'animation': db.Post.TYPE_ANIMATION,
'animated': db.Post.TYPE_ANIMATION,
'anim': db.Post.TYPE_ANIMATION,
'gif': db.Post.TYPE_ANIMATION,
'video': db.Post.TYPE_VIDEO,
'webm': db.Post.TYPE_VIDEO,
'flash': db.Post.TYPE_FLASH,
'swf': db.Post.TYPE_FLASH,
}
try:
return available_types[value.lower()]
except KeyError:
raise errors.SearchError('Invalid type: %r. Available types: %r.' % (
value, available_types))
class PostSearchConfig(BaseSearchConfig):
def create_query(self):
return db.session.query(db.Post)
def finalize_query(self, query):
return query.order_by(db.Post.creation_time.desc())
@property
def anonymous_filter(self):
return self._create_subquery_filter(
db.Post.post_id,
db.PostTag.post_id,
db.TagName.name,
self._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(
db.Post.post_id,
db.PostTag.post_id,
db.TagName.name,
self._create_str_filter,
lambda subquery: subquery.join(db.Tag).join(db.TagName)),
'score': self._create_num_filter(db.Post.score),
('uploader', 'upload', 'submit'):
self._create_subquery_filter(
db.Post.user_id,
db.User.user_id,
db.User.name,
self._create_str_filter),
'comment': self._create_subquery_filter(
db.Post.post_id,
db.Comment.post_id,
db.User.name,
self._create_str_filter,
lambda subquery: subquery.join(db.User)),
'fav': self._create_subquery_filter(
db.Post.post_id,
db.PostFavorite.post_id,
db.User.name,
self._create_str_filter,
lambda subquery: subquery.join(db.User)),
'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),
('image-width', 'width'):
self._create_num_filter(db.Post.canvas_width),
('image-height', 'height'):
self._create_num_filter(db.Post.canvas_height),
('image-area', 'area'):
self._create_num_filter(db.Post.canvas_area),
('creation-date', 'creation-time', 'date', 'time'):
self._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),
('comment-date', 'comment-time'):
self._create_date_filter(db.Post.last_comment_edit_time),
('fav-date', 'fav-time'):
self._create_date_filter(db.Post.last_favorite_time),
('feature-date', 'feature-time'):
self._create_date_filter(db.Post.last_feature_time),
})
@property
def sort_columns(self):
return util.unalias_dict({
'random': (func.random(), None),
'id': (db.Post.post_id, self.SORT_DESC),
'score': (db.Post.score, self.SORT_DESC),
'tag-count': (db.Post.tag_count, self.SORT_DESC),
'comment-count': (db.Post.comment_count, self.SORT_DESC),
'fav-count': (db.Post.favorite_count, self.SORT_DESC),
'note-count': (db.Post.note_count, self.SORT_DESC),
'feature-count': (db.Post.feature_count, self.SORT_DESC),
'file-size': (db.Post.file_size, self.SORT_DESC),
('image-width', 'width'): (db.Post.canvas_width, self.SORT_DESC),
('image-height', 'height'): (db.Post.canvas_height, self.SORT_DESC),
('image-area', 'area'): (db.Post.canvas_area, self.SORT_DESC),
('creation-date', 'creation-time', 'date', 'time'):
(db.Post.creation_time, self.SORT_DESC),
('last-edit-date', 'last-edit-time', 'edit-date', 'edit-time'):
(db.Post.last_edit_time, self.SORT_DESC),
('comment-date', 'comment-time'):
(db.Post.last_comment_edit_time, self.SORT_DESC),
('fav-date', 'fav-time'):
(db.Post.last_favorite_time, self.SORT_DESC),
('feature-date', 'feature-time'):
(db.Post.last_feature_time, self.SORT_DESC),
})
@property
def special_filters(self):
return {
'liked': self.own_liked_filter,
'disliked': self.own_disliked_filter,
'fav': self.own_fav_filter,
'tumbleweed': self.tumbleweed_filter,
}
def own_liked_filter(self, query, negated):
assert self.user
if self.user.rank == 'anonymous':
raise errors.SearchError('Must be logged in to use this feature.')
expr = db.Post.post_id.in_(
db.session \
.query(db.PostScore.post_id) \
.filter(db.PostScore.user_id == self.user.user_id) \
.filter(db.PostScore.score == 1))
if negated:
expr = ~expr
return query.filter(expr)
def own_disliked_filter(self, query, negated):
assert self.user
if self.user.rank == 'anonymous':
raise errors.SearchError('Must be logged in to use this feature.')
expr = db.Post.post_id.in_(
db.session \
.query(db.PostScore.post_id) \
.filter(db.PostScore.user_id == self.user.user_id) \
.filter(db.PostScore.score == -1))
if negated:
expr = ~expr
return query.filter(expr)
def own_fav_filter(self, query, negated):
assert self.user
if self.user.rank == 'anonymous':
raise errors.SearchError('Must be logged in to use this feature.')
expr = db.Post.post_id.in_(
db.session \
.query(db.PostFavorite.post_id) \
.filter(db.PostFavorite.user_id == self.user.user_id))
if negated:
expr = ~expr
return query.filter(expr)
def tumbleweed_filter(self, query, negated):
expr = \
(db.Post.comment_count == 0) \
& (db.Post.favorite_count == 0) \
& (db.Post.score == 0)
if negated:
expr = ~expr
return query.filter(expr)

View file

@ -10,7 +10,7 @@ class SearchExecutor(object):
''' '''
def __init__(self, search_config): def __init__(self, search_config):
self._search_config = search_config self.config = search_config
def execute(self, query_text, page, page_size): def execute(self, query_text, page, page_size):
''' '''
@ -43,7 +43,7 @@ class SearchExecutor(object):
def _prepare(self, query_text): def _prepare(self, query_text):
''' Parse input and return SQLAlchemy query. ''' ''' Parse input and return SQLAlchemy query. '''
query = self._search_config.create_query() \ query = self.config.create_query() \
.options(sqlalchemy.orm.lazyload('*')) .options(sqlalchemy.orm.lazyload('*'))
for token in re.split(r'\s+', (query_text or '').lower()): for token in re.split(r'\s+', (query_text or '').lower()):
if not token: if not token:
@ -60,7 +60,7 @@ class SearchExecutor(object):
query = self._handle_anonymous( query = self._handle_anonymous(
query, self._create_criterion(token, negated)) query, self._create_criterion(token, negated))
query = self._search_config.finalize_query(query) query = self.config.finalize_query(query)
return query return query
def _handle_key_value(self, query, key, value, negated): def _handle_key_value(self, query, key, value, negated):
@ -72,10 +72,10 @@ class SearchExecutor(object):
return self._handle_named(query, key, value, negated) return self._handle_named(query, key, value, negated)
def _handle_anonymous(self, query, criterion): def _handle_anonymous(self, query, criterion):
if not self._search_config.anonymous_filter: if not self.config.anonymous_filter:
raise errors.SearchError( raise errors.SearchError(
'Anonymous tokens are not valid in this context.') 'Anonymous tokens are not valid in this context.')
return self._search_config.anonymous_filter(query, criterion) return self.config.anonymous_filter(query, criterion)
def _handle_named(self, query, key, value, negated): def _handle_named(self, query, key, value, negated):
if key.endswith('-min'): if key.endswith('-min'):
@ -85,19 +85,18 @@ class SearchExecutor(object):
key = key[:-4] key = key[:-4]
value = '..' + value value = '..' + value
criterion = self._create_criterion(value, negated) criterion = self._create_criterion(value, negated)
if key in self._search_config.named_filters: if key in self.config.named_filters:
return self._search_config.named_filters[key](query, criterion) return self.config.named_filters[key](query, criterion)
raise errors.SearchError( raise errors.SearchError(
'Unknown named token: %r. Available named tokens: %r.' % ( 'Unknown named token: %r. Available named tokens: %r.' % (
key, list(self._search_config.named_filters.keys()))) key, list(self.config.named_filters.keys())))
def _handle_special(self, query, value, negated): def _handle_special(self, query, value, negated):
if value in self._search_config.special_filters: if value in self.config.special_filters:
return self._search_config.special_filters[value]( return self.config.special_filters[value](query, negated)
query, value, negated)
raise errors.SearchError( raise errors.SearchError(
'Unknown special token: %r. Available special tokens: %r.' % ( 'Unknown special token: %r. Available special tokens: %r.' % (
value, list(self._search_config.special_filters.keys()))) value, list(self.config.special_filters.keys())))
def _handle_sort(self, query, value, negated): def _handle_sort(self, query, value, negated):
if value.count(',') == 0: if value.count(',') == 0:
@ -108,14 +107,14 @@ class SearchExecutor(object):
raise errors.SearchError('Too many commas in sort style token.') raise errors.SearchError('Too many commas in sort style token.')
try: try:
column, default_sort = self._search_config.sort_columns[value] column, default_sort = self.config.sort_columns[value]
except KeyError: except KeyError:
raise errors.SearchError( raise errors.SearchError(
'Unknown sort style: %r. Available sort styles: %r.' % ( 'Unknown sort style: %r. Available sort styles: %r.' % (
value, list(self._search_config.sort_columns.keys()))) value, list(self.config.sort_columns.keys())))
sort_asc = self._search_config.SORT_ASC sort_asc = self.config.SORT_ASC
sort_desc = self._search_config.SORT_DESC sort_desc = self.config.SORT_DESC
try: try:
sort_map = { sort_map = {

View file

@ -21,9 +21,62 @@ def test_ctx(
ret.context_factory = context_factory ret.context_factory = context_factory
ret.user_factory = user_factory ret.user_factory = user_factory
ret.post_factory = post_factory ret.post_factory = post_factory
ret.list_api = api.PostListApi()
ret.detail_api = api.PostDetailApi() ret.detail_api = api.PostDetailApi()
return ret return ret
def test_retrieving_multiple(test_ctx):
post1 = test_ctx.post_factory(id=1)
post2 = test_ctx.post_factory(id=2)
db.session.add_all([post1, post2])
result = test_ctx.list_api.get(
test_ctx.context_factory(
input={'query': '', 'page': 1},
user=test_ctx.user_factory(rank='regular_user')))
assert result['query'] == ''
assert result['page'] == 1
assert result['pageSize'] == 100
assert result['total'] == 2
assert [t['id'] for t in result['results']] == [1, 2]
def test_using_special_tokens(
test_ctx, config_injector):
auth_user = test_ctx.user_factory(rank='regular_user')
post1 = test_ctx.post_factory(id=1)
post2 = test_ctx.post_factory(id=2)
post1.favorited_by = [db.PostFavorite(
user=auth_user, time=datetime.datetime.now())]
db.session.add_all([post1, post2, auth_user])
db.session.flush()
result = test_ctx.list_api.get(
test_ctx.context_factory(
input={'query': 'special:fav', 'page': 1},
user=auth_user))
assert result['query'] == 'special:fav'
assert result['page'] == 1
assert result['pageSize'] == 100
assert result['total'] == 1
assert [t['id'] for t in result['results']] == [1]
def test_trying_to_use_special_tokens_without_logging_in(
test_ctx, config_injector):
config_injector({
'privileges': {'posts:list': 'anonymous'},
'ranks': ['anonymous'],
})
with pytest.raises(errors.SearchError):
test_ctx.list_api.get(
test_ctx.context_factory(
input={'query': 'special:fav', 'page': 1},
user=test_ctx.user_factory(rank='anonymous')))
def test_trying_to_retrieve_multiple_without_privileges(test_ctx):
with pytest.raises(errors.AuthError):
test_ctx.list_api.get(
test_ctx.context_factory(
input={'query': '', 'page': 1},
user=test_ctx.user_factory(rank='anonymous')))
def test_retrieving_single(test_ctx): def test_retrieving_single(test_ctx):
db.session.add(test_ctx.post_factory(id=1)) db.session.add(test_ctx.post_factory(id=1))
result = test_ctx.detail_api.get( result = test_ctx.detail_api.get(

View file

@ -0,0 +1,578 @@
import datetime
import pytest
from szurubooru import db, errors, search
@pytest.fixture
def fav_factory(user_factory):
def factory(post, user=None):
return db.PostFavorite(
post=post, user=user or user_factory(), time=datetime.datetime.now())
return factory
@pytest.fixture
def score_factory(user_factory):
def factory(post, user=None, score=1):
return db.PostScore(
post=post,
user=user or user_factory(),
time=datetime.datetime.now(),
score=score)
return factory
@pytest.fixture
def note_factory():
def factory(post=None):
if post:
return db.PostNote(polygon='...', text='...', post=post)
return db.PostNote(polygon='...', text='...')
return factory
@pytest.fixture
def feature_factory(user_factory):
def factory(post=None):
if post:
return db.PostFeature(
time=datetime.datetime.now(), user=user_factory(), post=post)
return db.PostFeature(time=datetime.datetime.now(), user=user_factory())
return factory
@pytest.fixture
def executor(user_factory):
return search.SearchExecutor(search.PostSearchConfig())
@pytest.fixture
def auth_executor(executor, user_factory):
def wrapper():
auth_user = user_factory()
db.session.add(auth_user)
db.session.flush()
executor.config.user = auth_user
return auth_user
return wrapper
@pytest.fixture
def verify_unpaged(executor):
def verify(input, expected_post_ids, test_order=False):
actual_count, actual_posts = executor.execute(
input, page=1, page_size=100)
actual_post_ids = list([p.post_id for p in actual_posts])
print(actual_post_ids, expected_post_ids)
assert actual_count == len(expected_post_ids)
if not test_order:
actual_post_ids = sorted(actual_post_ids)
expected_post_ids = sorted(expected_post_ids)
assert actual_post_ids == expected_post_ids
return verify
@pytest.mark.parametrize('input,expected_post_ids', [
('id:1', [1]),
('id:3', [3]),
('id:1,3', [1, 3]),
])
def test_filter_by_id(verify_unpaged, post_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('tag:t1', [1]),
('tag:t2', [2]),
('tag:t1,t2', [1, 2]),
('tag:t4a', [4]),
('tag:t4b', [4]),
])
def test_filter_by_tag(
verify_unpaged, post_factory, tag_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post4 = post_factory(id=4)
post1.tags=[tag_factory(names=['t1'])]
post2.tags=[tag_factory(names=['t2'])]
post3.tags=[tag_factory(names=['t3'])]
post4.tags=[tag_factory(names=['t4a', 't4b'])]
db.session.add_all([post1, post2, post3, post4])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('score:1', [1]),
('score:3', [3]),
('score:1,3', [1, 3]),
])
def test_filter_by_score(
verify_unpaged, post_factory, user_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
for post in [post1, post2, post3]:
db.session.add(
db.PostScore(
score=post.post_id,
time=datetime.datetime.now(),
post=post,
user=user_factory()))
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('uploader:u1', [1]),
('uploader:u3', [3]),
('uploader:u1,u3', [1, 3]),
('upload:u1', [1]),
('upload:u3', [3]),
('upload:u1,u3', [1, 3]),
('submit:u1', [1]),
('submit:u3', [3]),
('submit:u1,u3', [1, 3]),
])
def test_filter_by_uploader(
verify_unpaged, post_factory, user_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.user = user_factory(name='u1')
post2.user = user_factory(name='u2')
post3.user = user_factory(name='u3')
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('comment:u1', [1]),
('comment:u3', [3]),
('comment:u1,u3', [1, 3]),
])
def test_filter_by_commenter(
verify_unpaged,
post_factory,
user_factory,
comment_factory,
input,
expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([
comment_factory(post=post1, user=user_factory(name='u1')),
comment_factory(post=post2, user=user_factory(name='u2')),
comment_factory(post=post3, user=user_factory(name='u3')),
post1, post2, post3,
])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('fav:u1', [1]),
('fav:u3', [3]),
('fav:u1,u3', [1, 3]),
])
def test_filter_by_favorite(
verify_unpaged,
post_factory,
user_factory,
fav_factory,
input,
expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([
fav_factory(post=post1, user=user_factory(name='u1')),
fav_factory(post=post2, user=user_factory(name='u2')),
fav_factory(post=post3, user=user_factory(name='u3')),
post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('tag-count:1', [1]),
('tag-count:3', [3]),
('tag-count:1,3', [1, 3]),
])
def test_filter_by_tag_count(
verify_unpaged, post_factory, tag_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.tags=[tag_factory()]
post2.tags=[tag_factory(), tag_factory()]
post3.tags=[tag_factory(), tag_factory(), tag_factory()]
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('comment-count:1', [1]),
('comment-count:3', [3]),
('comment-count:1,3', [1, 3]),
])
def test_filter_by_comment_count(
verify_unpaged, post_factory, comment_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([
comment_factory(post=post1),
comment_factory(post=post2),
comment_factory(post=post2),
comment_factory(post=post3),
comment_factory(post=post3),
comment_factory(post=post3),
post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('fav-count:1', [1]),
('fav-count:3', [3]),
('fav-count:1,3', [1, 3]),
])
def test_filter_by_favorite_count(
verify_unpaged, post_factory, fav_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([
fav_factory(post=post1),
fav_factory(post=post2),
fav_factory(post=post2),
fav_factory(post=post3),
fav_factory(post=post3),
fav_factory(post=post3),
post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('note-count:1', [1]),
('note-count:3', [3]),
('note-count:1,3', [1, 3]),
])
def test_filter_by_note_count(
verify_unpaged, post_factory, note_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.notes=[note_factory()]
post2.notes=[note_factory(), note_factory()]
post3.notes=[note_factory(), note_factory(), note_factory()]
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('feature-count:1', [1]),
('feature-count:3', [3]),
('feature-count:1,3', [1, 3]),
])
def test_filter_by_feature_count(
verify_unpaged, post_factory, feature_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.features=[feature_factory()]
post2.features=[feature_factory(), feature_factory()]
post3.features=[feature_factory(), feature_factory(), feature_factory()]
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('type:image', [1]),
('type:anim', [2]),
('type:animation', [2]),
('type:gif', [2]),
('type:video', [3]),
('type:webm', [3]),
('type:flash', [4]),
('type:swf', [4]),
])
def test_filter_by_type(verify_unpaged, post_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post4 = post_factory(id=4)
post1.type = db.Post.TYPE_IMAGE
post2.type = db.Post.TYPE_ANIMATION
post3.type = db.Post.TYPE_VIDEO
post4.type = db.Post.TYPE_FLASH
db.session.add_all([post1, post2, post3, post4])
verify_unpaged(input, expected_post_ids)
def test_filter_by_invalid_type(executor):
with pytest.raises(errors.SearchError):
actual_count, actual_posts = executor.execute(
'type:invalid', page=1, page_size=100)
@pytest.mark.parametrize('input,expected_post_ids', [
('file-size:100', [1]),
('file-size:102', [3]),
('file-size:100,102', [1, 3]),
])
def test_filter_by_file_size(
verify_unpaged, post_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.file_size = 100
post2.file_size = 101
post3.file_size = 102
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('image-width:100', [1]),
('image-width:102', [3]),
('image-width:100,102', [1, 3]),
('image-height:200', [1]),
('image-height:202', [3]),
('image-height:200,202', [1, 3]),
('image-area:20000', [1]),
('image-area:20604', [3]),
('image-area:20000,20604', [1, 3]),
])
def test_filter_by_image_size(
verify_unpaged, post_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.canvas_width = 100
post2.canvas_width = 101
post3.canvas_width = 102
post1.canvas_height = 200
post2.canvas_height = 201
post3.canvas_height = 202
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('creation-date:2014', [1]),
('creation-date:2016', [3]),
('creation-date:2014,2016', [1, 3]),
('creation-time:2014', [1]),
('creation-time:2016', [3]),
('creation-time:2014,2016', [1, 3]),
('date:2014', [1]),
('date:2016', [3]),
('date:2014,2016', [1, 3]),
('time:2014', [1]),
('time:2016', [3]),
('time:2014,2016', [1, 3]),
])
def test_filter_by_creation_time(
verify_unpaged, post_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.creation_time = datetime.datetime(2014, 1, 1)
post2.creation_time = datetime.datetime(2015, 1, 1)
post3.creation_time = datetime.datetime(2016, 1, 1)
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('last-edit-date:2014', [1]),
('last-edit-date:2016', [3]),
('last-edit-date:2014,2016', [1, 3]),
('last-edit-time:2014', [1]),
('last-edit-time:2016', [3]),
('last-edit-time:2014,2016', [1, 3]),
('edit-date:2014', [1]),
('edit-date:2016', [3]),
('edit-date:2014,2016', [1, 3]),
('edit-time:2014', [1]),
('edit-time:2016', [3]),
('edit-time:2014,2016', [1, 3]),
])
def test_filter_by_last_edit_time(
verify_unpaged, post_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post1.last_edit_time = datetime.datetime(2014, 1, 1)
post2.last_edit_time = datetime.datetime(2015, 1, 1)
post3.last_edit_time = datetime.datetime(2016, 1, 1)
db.session.add_all([post1, post2, post3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('comment-date:2014', [1]),
('comment-date:2016', [3]),
('comment-date:2014,2016', [1, 3]),
('comment-time:2014', [1]),
('comment-time:2016', [3]),
('comment-time:2014,2016', [1, 3]),
])
def test_filter_by_comment_date(
verify_unpaged, post_factory, comment_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
comment1 = comment_factory(post=post1)
comment2 = comment_factory(post=post2)
comment3 = comment_factory(post=post3)
comment1.last_edit_time = datetime.datetime(2014, 1, 1)
comment2.last_edit_time = datetime.datetime(2015, 1, 1)
comment3.last_edit_time = datetime.datetime(2016, 1, 1)
db.session.add_all([post1, post2, post3, comment1, comment2, comment3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('fav-date:2014', [1]),
('fav-date:2016', [3]),
('fav-date:2014,2016', [1, 3]),
('fav-time:2014', [1]),
('fav-time:2016', [3]),
('fav-time:2014,2016', [1, 3]),
])
def test_filter_by_fav_date(
verify_unpaged, post_factory, fav_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
fav1 = fav_factory(post=post1)
fav2 = fav_factory(post=post2)
fav3 = fav_factory(post=post3)
fav1.time = datetime.datetime(2014, 1, 1)
fav2.time = datetime.datetime(2015, 1, 1)
fav3.time = datetime.datetime(2016, 1, 1)
db.session.add_all([post1, post2, post3, fav1, fav2, fav3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input,expected_post_ids', [
('feature-date:2014', [1]),
('feature-date:2016', [3]),
('feature-date:2014,2016', [1, 3]),
('feature-time:2014', [1]),
('feature-time:2016', [3]),
('feature-time:2014,2016', [1, 3]),
])
def test_filter_by_feature_date(
verify_unpaged, post_factory, feature_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
feature1 = feature_factory(post=post1)
feature2 = feature_factory(post=post2)
feature3 = feature_factory(post=post3)
feature1.time = datetime.datetime(2014, 1, 1)
feature2.time = datetime.datetime(2015, 1, 1)
feature3.time = datetime.datetime(2016, 1, 1)
db.session.add_all([post1, post2, post3, feature1, feature2, feature3])
verify_unpaged(input, expected_post_ids)
@pytest.mark.parametrize('input', [
'sort:random',
'sort:id',
'sort:score',
'sort:tag-count',
'sort:comment-count',
'sort:fav-count',
'sort:note-count',
'sort:feature-count',
'sort:file-size',
'sort:image-width',
'sort:width',
'sort:image-height',
'sort:height',
'sort:image-area',
'sort:area',
'sort:creation-date',
'sort:creation-time',
'sort:date',
'sort:time',
'sort:last-edit-date',
'sort:last-edit-time',
'sort:edit-date',
'sort:edit-time',
'sort:comment-date',
'sort:comment-time',
'sort:fav-date',
'sort:fav-time',
'sort:feature-date',
'sort:feature-time',
])
def test_sort_tokens(verify_unpaged, post_factory, input):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([post1, post2, post3])
verify_unpaged(input, [1, 2, 3])
@pytest.mark.parametrize('input,expected_post_ids', [
('', [1, 2, 3, 4]),
('t1', [1]),
('t2', [2]),
('t1,t2', [1, 2]),
('t4a', [4]),
('t4b', [4]),
])
def test_anonymous(
verify_unpaged, post_factory, tag_factory, input, expected_post_ids):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post4 = post_factory(id=4)
post1.tags=[tag_factory(names=['t1'])]
post2.tags=[tag_factory(names=['t2'])]
post3.tags=[tag_factory(names=['t3'])]
post4.tags=[tag_factory(names=['t4a', 't4b'])]
db.session.add_all([post1, post2, post3, post4])
verify_unpaged(input, expected_post_ids)
def test_own_liked(
auth_executor, post_factory, score_factory, user_factory, verify_unpaged):
auth_user = auth_executor()
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([
score_factory(post=post1, user=auth_user, score=1),
score_factory(post=post2, user=user_factory(name='unrelated'), score=1),
score_factory(post=post3, user=auth_user, score=-1),
post1, post2, post3,
])
verify_unpaged('special:liked', [1])
verify_unpaged('-special:liked', [2, 3])
def test_own_disliked(
auth_executor, post_factory, score_factory, user_factory, verify_unpaged):
auth_user = auth_executor()
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
db.session.add_all([
score_factory(post=post1, user=auth_user, score=-1),
score_factory(post=post2, user=user_factory(name='unrelated'), score=-1),
score_factory(post=post3, user=auth_user, score=1),
post1, post2, post3,
])
verify_unpaged('special:disliked', [1])
verify_unpaged('-special:disliked', [2, 3])
def test_own_fav(
auth_executor, post_factory, fav_factory, user_factory, verify_unpaged):
auth_user = auth_executor()
post1 = post_factory(id=1)
post2 = post_factory(id=2)
db.session.add_all([
fav_factory(post=post1, user=auth_user),
fav_factory(post=post2, user=user_factory(name='unrelated')),
post1, post2,
])
verify_unpaged('special:fav', [1])
verify_unpaged('-special:fav', [2])
def test_tumbleweed(
executor,
post_factory,
fav_factory,
comment_factory,
score_factory,
verify_unpaged):
post1 = post_factory(id=1)
post2 = post_factory(id=2)
post3 = post_factory(id=3)
post4 = post_factory(id=4)
db.session.add_all([
comment_factory(post=post1),
score_factory(post=post2),
fav_factory(post=post3),
post1, post2, post3, post4,
])
verify_unpaged('special:tumbleweed', [4])
verify_unpaged('-special:tumbleweed', [1, 2, 3])