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)
- [Listing tag siblings](#listing-tag-siblings)
- Posts
- ~~Listing posts~~
- [Listing posts](#listing-posts)
- [Creating post](#creating-post)
- [Updating post](#updating-post)
- [Getting post](#getting-post)
@ -290,7 +290,7 @@ data.
**Named tokens**
| `<value>` | Description |
| `<key>` | Description |
| ------------------- | ------------------------------------- |
| `name` | having given name (accepts wildcards) |
| `category` | having given category |
@ -516,6 +516,111 @@ data.
appears with given tag. Results are sorted by occurrences count and the
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
- **Request**
@ -770,11 +875,12 @@ data.
**Named tokens**
| `<value>` | Description |
| `<key>` | Description |
| ---------------- | ---------------------------------------------- |
| `id` | specific comment ID |
| `post` | specific post ID |
| `user` | created by given user (accepts wildcards) |
| `author` | alias of `user` |
| `text` | containing given text (accepts wildcards) |
| `creation-date` | created at given date |
| `creation-time` | alias of `creation-date` |
@ -789,6 +895,7 @@ data.
| ---------------- | ------------------------- |
| `random` | as random as it can get |
| `user` | author name, A to Z |
| `author` | alias of `user` |
| `post` | post ID, newest to oldest |
| `creation-date` | newest to oldest |
| `creation-time` | alias of `creation-date` |
@ -946,7 +1053,7 @@ data.
**Named tokens**
| `<value>` | Description |
| `<key>` | Description |
| ----------------- | ----------------------------------------------- |
| `name` | having given name (accepts wildcards) |
| `creation-date` | registered at given date |
@ -1182,7 +1289,7 @@ data.
**Named tokens**
| `<value>` | Description |
| `<key>` | Description |
| ----------------- | --------------------------------------------- |
| `type` | involving given resource type |
| `id` | involving given resource id |

View file

@ -1,6 +1,6 @@
<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>
@ -8,7 +8,11 @@
<tbody>
<tr>
<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>
<td><code>score</code></td>
@ -18,6 +22,14 @@
<td><code>uploader</code></td>
<td>uploaded by given user</td>
</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>
<td><code>comment</code></td>
<td>commented by given user</td>
@ -27,16 +39,16 @@
<td>favorited by given user</td>
</tr>
<tr>
<td><code>fav-count</code></td>
<td>favorited by given number of users</td>
<td><code>tag-count</code></td>
<td>having given number of tags</td>
</tr>
<tr>
<td><code>comment-count</code></td>
<td>having given number of comments</td>
</tr>
<tr>
<td><code>tag-count</code></td>
<td>having given number of tags</td>
<td><code>fav-count</code></td>
<td>favorited by given number of users</td>
</tr>
<tr>
<td><code>note-count</code></td>
@ -47,8 +59,8 @@
<td>having been featured given number of times</td>
</tr>
<tr>
<td><code>date</code></td>
<td>posted at given date</td>
<td><code>type</code></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>
<td><code>file-size</code></td>
@ -67,8 +79,72 @@
<td>having given number of pixels (image width * image height)</td>
</tr>
<tr>
<td><code>type</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><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>
<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>
</tbody>
</table>
@ -83,28 +159,32 @@
</tr>
<tr>
<td><code>id</code></td>
<td>highest to lowest post ID</td>
<td>highest to lowest post number</td>
</tr>
<tr>
<td><code>score</code></td>
<td>highest scored</td>
</tr>
<tr>
<td><code>fav-count</code></td>
<td>loved by most</td>
<td><code>tag-count</code></td>
<td>with most tags</td>
</tr>
<tr>
<td><code>comment-count</code></td>
<td>most commented first</td>
</tr>
<tr>
<td><code>tag-count</code></td>
<td>with most tags</td>
<td><code>fav-count</code></td>
<td>loved by most</td>
</tr>
<tr>
<td><code>note-count</code></td>
<td>with most annotations</td>
</tr>
<tr>
<td><code>feature-count</code></td>
<td>most often featured</td>
</tr>
<tr>
<td><code>file-size</code></td>
<td>largest files first</td>
@ -121,29 +201,73 @@
<td><code>image-area</code></td>
<td>largest images first</td>
</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>
<td><code>creation-date</code></td>
<td>newest to oldest (pretty much same as <code>id</code>)</td>
</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>
</tr>
<tr>
<td><code>fav-date</code></td>
<td>recently added to favorites by anyone</td>
<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>recently commented by anyone</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>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>
<td><code>feature-date</code></td>
<td>recently featured</td>
</tr>
<tr>
<td><code>feature-count</code></td>
<td>most often featured</td>
<td><code>feature-time</code></td>
<td>alias of <code>feature-time</code></td>
</tr>
</tbody>
</table>

View file

@ -1,8 +1,19 @@
import datetime
from szurubooru import search
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, tags, posts, snapshots, favorites, scores
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):
auth.verify_privilege(ctx.user, 'posts:create')
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.sql.expression import func, select
from szurubooru.db.base import Base
from szurubooru.db.comment import Comment
class PostFeature(Base):
__tablename__ = 'post_feature'
@ -117,6 +118,8 @@ class Post(Base):
.where(PostTag.post_id == post_id) \
.correlate_except(PostTag))
canvas_area = column_property(canvas_width * canvas_height)
@property
def is_featured(self):
featured_post = object_session(self) \
@ -125,12 +128,10 @@ class Post(Base):
.first()
return featured_post and featured_post.post_id == self.post_id
@property
def score(self):
return object_session(self) \
.query(func.sum(PostScore.score)) \
.filter(PostScore.post_id == self.post_id) \
.one()[0] or 0
score = column_property(
select([func.coalesce(func.sum(PostScore.score), 0)]) \
.where(PostScore.post_id == post_id) \
.correlate_except(PostScore))
favorite_count = column_property(
select([func.count(PostFavorite.post_id)]) \
@ -152,10 +153,22 @@ class Post(Base):
.where(PostFeature.post_id == post_id) \
.correlate_except(PostFeature))
# TODO: wire these
#comment_count = Column('auto_comment_count', Integer, nullable=False, default=0)
#note_count = Column('auto_note_count', Integer, nullable=False, default=0)
#last_comment_edit_time = Column(
# 'auto_comment_creation_time', Integer, nullable=False, default=0)
#last_comment_creation_time = Column(
# 'auto_comment_edit_time', Integer, nullable=False, default=0)
comment_count = column_property(
select([func.count(Comment.post_id)]) \
.where(Comment.post_id == post_id) \
.correlate_except(Comment))
last_comment_creation_time = column_property(
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 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):
if not isinstance(source, bytes):
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.tag_search_config import TagSearchConfig
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 szurubooru.errors
from szurubooru import db
from szurubooru.func import util
from szurubooru.search import criteria
def wildcard_transformer(value):
return value.replace('*', '%')
class BaseSearchConfig(object):
SORT_DESC = -1
SORT_ASC = 1
@ -50,17 +54,16 @@ class BaseSearchConfig(object):
BaseSearchConfig._apply_num_criterion_to_column(column, criterion))
@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.
Parse potential wildcards inside the criterion.
'''
if isinstance(criterion, criteria.PlainSearchCriterion):
expr = column.like(criterion.value.replace('*', '%'))
expr = column.like(transformer(criterion.value))
elif isinstance(criterion, criteria.ArraySearchCriterion):
expr = sqlalchemy.sql.false()
for value in criterion.values:
expr = expr | column.like(value.replace('*', '%'))
expr = expr | column.like(transformer(value))
elif isinstance(criterion, criteria.RangedSearchCriterion):
raise szurubooru.errors.SearchError(
'Composite token %r is invalid in this context.' % (criterion,))
@ -71,9 +74,10 @@ class BaseSearchConfig(object):
return expr
@staticmethod
def _create_str_filter(column):
def _create_str_filter(column, transformer=wildcard_transformer):
return lambda query, criterion: query.filter(
BaseSearchConfig._apply_str_criterion_to_column(column, criterion))
BaseSearchConfig._apply_str_criterion_to_column(
column, criterion, transformer))
@staticmethod
def _apply_date_criterion_to_column(column, criterion):
@ -111,3 +115,21 @@ class BaseSearchConfig(object):
def _create_date_filter(column):
return lambda query, criterion: query.filter(
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),
'post': self._create_num_filter(db.Comment.post_id),
'user': self._create_str_filter(db.User.name),
'author': self._create_str_filter(db.User.name),
'text': self._create_str_filter(db.Comment.text),
'creation-date': self._create_date_filter(db.Comment.creation_time),
'creation-time': self._create_date_filter(db.Comment.creation_time),
@ -33,6 +34,7 @@ class CommentSearchConfig(BaseSearchConfig):
return {
'random': (func.random(), None),
'user': (db.User.name, self.SORT_ASC),
'author': (db.User.name, self.SORT_ASC),
'post': (db.Comment.post_id, self.SORT_DESC),
'creation-date': (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):
self._search_config = search_config
self.config = search_config
def execute(self, query_text, page, page_size):
'''
@ -43,7 +43,7 @@ class SearchExecutor(object):
def _prepare(self, query_text):
''' Parse input and return SQLAlchemy query. '''
query = self._search_config.create_query() \
query = self.config.create_query() \
.options(sqlalchemy.orm.lazyload('*'))
for token in re.split(r'\s+', (query_text or '').lower()):
if not token:
@ -60,7 +60,7 @@ class SearchExecutor(object):
query = self._handle_anonymous(
query, self._create_criterion(token, negated))
query = self._search_config.finalize_query(query)
query = self.config.finalize_query(query)
return query
def _handle_key_value(self, query, key, value, negated):
@ -72,10 +72,10 @@ class SearchExecutor(object):
return self._handle_named(query, key, value, negated)
def _handle_anonymous(self, query, criterion):
if not self._search_config.anonymous_filter:
if not self.config.anonymous_filter:
raise errors.SearchError(
'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):
if key.endswith('-min'):
@ -85,19 +85,18 @@ class SearchExecutor(object):
key = key[:-4]
value = '..' + value
criterion = self._create_criterion(value, negated)
if key in self._search_config.named_filters:
return self._search_config.named_filters[key](query, criterion)
if key in self.config.named_filters:
return self.config.named_filters[key](query, criterion)
raise errors.SearchError(
'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):
if value in self._search_config.special_filters:
return self._search_config.special_filters[value](
query, value, negated)
if value in self.config.special_filters:
return self.config.special_filters[value](query, negated)
raise errors.SearchError(
'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):
if value.count(',') == 0:
@ -108,14 +107,14 @@ class SearchExecutor(object):
raise errors.SearchError('Too many commas in sort style token.')
try:
column, default_sort = self._search_config.sort_columns[value]
column, default_sort = self.config.sort_columns[value]
except KeyError:
raise errors.SearchError(
'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_desc = self._search_config.SORT_DESC
sort_asc = self.config.SORT_ASC
sort_desc = self.config.SORT_DESC
try:
sort_map = {

View file

@ -21,9 +21,62 @@ def test_ctx(
ret.context_factory = context_factory
ret.user_factory = user_factory
ret.post_factory = post_factory
ret.list_api = api.PostListApi()
ret.detail_api = api.PostDetailApi()
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):
db.session.add(test_ctx.post_factory(id=1))
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])