server/posts: add post featuring
This commit is contained in:
parent
a30886cc70
commit
cf00a3a2de
13 changed files with 429 additions and 19 deletions
129
API.md
129
API.md
|
@ -27,6 +27,16 @@
|
||||||
- [Deleting tag](#deleting-tag)
|
- [Deleting tag](#deleting-tag)
|
||||||
- [Merging tags](#merging-tags)
|
- [Merging tags](#merging-tags)
|
||||||
- [Listing tag siblings](#listing-tag-siblings)
|
- [Listing tag siblings](#listing-tag-siblings)
|
||||||
|
- Posts
|
||||||
|
- ~~Listing posts~~
|
||||||
|
- ~~Creating post~~
|
||||||
|
- ~~Updating post~~
|
||||||
|
- ~~Getting post~~
|
||||||
|
- ~~Deleting post~~
|
||||||
|
- ~~Scoring posts~~
|
||||||
|
- ~~Adding posts to favorites~~
|
||||||
|
- ~~Removing posts from favorites~~
|
||||||
|
- [Featuring post](#featuring-post)
|
||||||
- Users
|
- Users
|
||||||
- [Listing users](#listing-users)
|
- [Listing users](#listing-users)
|
||||||
- [Creating user](#creating-user)
|
- [Creating user](#creating-user)
|
||||||
|
@ -44,6 +54,7 @@
|
||||||
- [User](#user)
|
- [User](#user)
|
||||||
- [Tag category](#tag-category)
|
- [Tag category](#tag-category)
|
||||||
- [Tag](#tag)
|
- [Tag](#tag)
|
||||||
|
- [Post](#post)
|
||||||
- [Snapshot](#snapshot)
|
- [Snapshot](#snapshot)
|
||||||
|
|
||||||
4. [Search](#search)
|
4. [Search](#search)
|
||||||
|
@ -590,6 +601,36 @@ data.
|
||||||
list is truncated to the first 50 elements. Doesn't use paging.
|
list is truncated to the first 50 elements. Doesn't use paging.
|
||||||
|
|
||||||
|
|
||||||
|
## Featuring post
|
||||||
|
- **Request**
|
||||||
|
|
||||||
|
`POST /featured-post`
|
||||||
|
|
||||||
|
- **Output**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"post": <post>,
|
||||||
|
"snapshots": [
|
||||||
|
<snapshot>,
|
||||||
|
<snapshot>,
|
||||||
|
<snapshot>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
...where `<post>` is a [post resource](#post), and `snapshots` contain its
|
||||||
|
earlier versions.
|
||||||
|
|
||||||
|
- **Errors**
|
||||||
|
|
||||||
|
- privileges are too low
|
||||||
|
- trying to feature a post that is currently featured
|
||||||
|
|
||||||
|
- **Description**
|
||||||
|
|
||||||
|
Features a post on the main page.
|
||||||
|
|
||||||
|
|
||||||
## Listing users
|
## Listing users
|
||||||
- **Request**
|
- **Request**
|
||||||
|
|
||||||
|
@ -1007,7 +1048,76 @@ A single tag. Tags are used to let users search for posts.
|
||||||
- `<suggestions>`: a list of suggested tag names. Suggested tags are shown to
|
- `<suggestions>`: a list of suggested tag names. Suggested tags are shown to
|
||||||
the user by the web client on usage.
|
the user by the web client on usage.
|
||||||
- `<creation-time>`: time the tag was created, formatted as per RFC 3339.
|
- `<creation-time>`: time the tag was created, formatted as per RFC 3339.
|
||||||
- `<creation-time>`: time the tag was edited, formatted as per RFC 3339.
|
- `<last-edit-time>`: time the tag was edited, formatted as per RFC 3339.
|
||||||
|
|
||||||
|
## Post
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
One file together with its metadata posted to the site.
|
||||||
|
|
||||||
|
**Structure**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"id": <id>,
|
||||||
|
"safety": <safety>,
|
||||||
|
"type": <type>,
|
||||||
|
"checksum": <checksum>,
|
||||||
|
"source": <source>,
|
||||||
|
"canvasWidth": <canvas-width>,
|
||||||
|
"canvasHeight": <canvas-height>,
|
||||||
|
"flags": <flags>,
|
||||||
|
"tags": <tags>,
|
||||||
|
"relations": <relations>,
|
||||||
|
"creationTime": <creation-time>,
|
||||||
|
"lastEditTime": <last-edit-time>,
|
||||||
|
"user": <user>,
|
||||||
|
"score": <score>,
|
||||||
|
"favoritedBy": <favorited-by>,
|
||||||
|
"featureCount": <feature-count>,
|
||||||
|
"lastFeatureTime": <last-feature-time>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field meaning**
|
||||||
|
|
||||||
|
- `<id>`: the post identifier.
|
||||||
|
- `<safety>`: whether the post is safe for work.
|
||||||
|
|
||||||
|
Available values:
|
||||||
|
|
||||||
|
- `"safe"`
|
||||||
|
- `"sketchy"`
|
||||||
|
- `"unsafe"`
|
||||||
|
|
||||||
|
- `<type>`: the type of the post.
|
||||||
|
|
||||||
|
Available values:
|
||||||
|
|
||||||
|
- `"image"` - plain image.
|
||||||
|
- `"animation"` - animated image (GIF).
|
||||||
|
- `"video"` - WEBM video.
|
||||||
|
- `"flash"` - Flash animation / game.
|
||||||
|
- `"youtube"` - Youtube embed.
|
||||||
|
|
||||||
|
- `<checksum>`: the file checksum. Used in snapshots to signify changes of the
|
||||||
|
post content.
|
||||||
|
- `<source>`: where the post was grabbed form, supplied by the user.
|
||||||
|
- `<canvas-width>` and `<canvas-height>`: the original width and height of the
|
||||||
|
post content.
|
||||||
|
- `<flags>`: various flags such as whether the post is looped, represented as
|
||||||
|
array of plain strings.
|
||||||
|
- `<tags>`: list of tag names the post is tagged with.
|
||||||
|
- `<relations>`: a list of related post IDs. Links to related posts are shown
|
||||||
|
to the user by the web client.
|
||||||
|
- `<creation-time>`: time the tag was created, formatted as per RFC 3339.
|
||||||
|
- `<last-edit-time>`: time the tag was edited, formatted as per RFC 3339.
|
||||||
|
- `<user>`: who created the post, serialized as [user resource](#user).
|
||||||
|
- `<score>`: the score (+1/-1 rating) of the given post.
|
||||||
|
- `<favorited-by>`: list of users, serialized as [user resources](#user).
|
||||||
|
- `<feature-count>`: how many times has the post been featured.
|
||||||
|
- `<last-feature-time>`: the last time the post was featured, formatted as per
|
||||||
|
RFC 3339.
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
**Description**
|
**Description**
|
||||||
|
@ -1078,6 +1188,23 @@ A snapshot is a version of a database resource.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Post snapshot data (`<resource-type> = "post"`)
|
||||||
|
|
||||||
|
*Example*
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"source": "http://example.com/",
|
||||||
|
"safety": "safe",
|
||||||
|
"checksum": "deadbeef",
|
||||||
|
"tags": ["tag1", "tag2"],
|
||||||
|
"relations": [1, 2],
|
||||||
|
"notes": [{"polygon": [[1,1],[200,1],[200,200],[1,200]], "text": "..."}],
|
||||||
|
"flags": ["loop"],
|
||||||
|
"featured": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- `<earlier-data>`: `<data>` field from the last snapshot of the same resource.
|
- `<earlier-data>`: `<data>` field from the last snapshot of the same resource.
|
||||||
This allows the client to create visual diffs for any given snapshot without
|
This allows the client to create visual diffs for any given snapshot without
|
||||||
the need to know any other snapshots for a given resource.
|
the need to know any other snapshots for a given resource.
|
||||||
|
|
|
@ -2,8 +2,15 @@
|
||||||
|
|
||||||
from szurubooru.api.password_reset_api import PasswordResetApi
|
from szurubooru.api.password_reset_api import PasswordResetApi
|
||||||
from szurubooru.api.user_api import UserListApi, UserDetailApi
|
from szurubooru.api.user_api import UserListApi, UserDetailApi
|
||||||
from szurubooru.api.tag_api import TagListApi, TagDetailApi, TagMergeApi, TagSiblingsApi
|
from szurubooru.api.tag_api import (
|
||||||
from szurubooru.api.tag_category_api import TagCategoryListApi, TagCategoryDetailApi
|
TagListApi,
|
||||||
|
TagDetailApi,
|
||||||
|
TagMergeApi,
|
||||||
|
TagSiblingsApi)
|
||||||
|
from szurubooru.api.tag_category_api import (
|
||||||
|
TagCategoryListApi,
|
||||||
|
TagCategoryDetailApi)
|
||||||
|
from szurubooru.api.post_api import PostFeatureApi
|
||||||
from szurubooru.api.snapshot_api import SnapshotListApi
|
from szurubooru.api.snapshot_api import SnapshotListApi
|
||||||
from szurubooru.api.info_api import InfoApi
|
from szurubooru.api.info_api import InfoApi
|
||||||
from szurubooru.api.context import Context, Request
|
from szurubooru.api.context import Context, Request
|
||||||
|
|
58
server/szurubooru/api/post_api.py
Normal file
58
server/szurubooru/api/post_api.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from szurubooru.api.base_api import BaseApi
|
||||||
|
from szurubooru.api.user_api import serialize_user
|
||||||
|
from szurubooru.func import auth, posts, snapshots
|
||||||
|
|
||||||
|
def serialize_post(post, authenticated_user):
|
||||||
|
ret = {
|
||||||
|
'id': post.post_id,
|
||||||
|
'creationTime': post.creation_time,
|
||||||
|
'lastEditTime': post.last_edit_time,
|
||||||
|
'safety': post.safety,
|
||||||
|
'type': post.type,
|
||||||
|
'checksum': post.checksum,
|
||||||
|
'source': post.source,
|
||||||
|
'fileSize': post.file_size,
|
||||||
|
'canvasWidth': post.canvas_width,
|
||||||
|
'canvasHeight': post.canvas_height,
|
||||||
|
'flags': post.flags,
|
||||||
|
'tags': [tag.first_name for tag in post.tags],
|
||||||
|
'relations': [rel.post_id for rel in post.relations],
|
||||||
|
'notes': sorted([{
|
||||||
|
'path': note.path,
|
||||||
|
'text': note.text,
|
||||||
|
} for note in post.notes]),
|
||||||
|
'user': serialize_user(post.user, authenticated_user),
|
||||||
|
'score': post.score,
|
||||||
|
'featureCount': post.feature_count,
|
||||||
|
'lastFeatureTime': post.last_feature_time,
|
||||||
|
'favoritedBy': [serialize_user(rel, authenticated_user) \
|
||||||
|
for rel in post.favorited_by],
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: fetch own score if needed
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def serialize_post_with_details(post, authenticated_user):
|
||||||
|
return {
|
||||||
|
'post': serialize_post(post, authenticated_user),
|
||||||
|
'snapshots': snapshots.get_serialized_history(post),
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostFeatureApi(BaseApi):
|
||||||
|
def post(self, ctx):
|
||||||
|
auth.verify_privilege(ctx.user, 'posts:feature')
|
||||||
|
post_id = ctx.get_param_as_int('id', required=True)
|
||||||
|
post = posts.get_post_by_id(post_id)
|
||||||
|
if not post:
|
||||||
|
raise posts.PostNotFoundError('Post %r not found.' % post_id)
|
||||||
|
featured_post = posts.get_featured_post()
|
||||||
|
if featured_post and featured_post.post_id == post.post_id:
|
||||||
|
raise posts.PostAlreadyFeaturedError(
|
||||||
|
'Post %r is already featured.' % post_id)
|
||||||
|
posts.feature_post(post, ctx.user)
|
||||||
|
if featured_post:
|
||||||
|
snapshots.modify(featured_post, ctx.user)
|
||||||
|
snapshots.modify(post, ctx.user)
|
||||||
|
ctx.session.commit()
|
||||||
|
return serialize_post_with_details(post, ctx.user)
|
|
@ -3,7 +3,10 @@ from szurubooru import config, search
|
||||||
from szurubooru.api.base_api import BaseApi
|
from szurubooru.api.base_api import BaseApi
|
||||||
from szurubooru.func import auth, users
|
from szurubooru.func import auth, users
|
||||||
|
|
||||||
def _serialize_user(authenticated_user, user):
|
def serialize_user(user, authenticated_user):
|
||||||
|
if not user:
|
||||||
|
return {}
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
'rank': user.rank,
|
'rank': user.rank,
|
||||||
|
@ -36,7 +39,7 @@ class UserListApi(BaseApi):
|
||||||
def get(self, ctx):
|
def get(self, ctx):
|
||||||
auth.verify_privilege(ctx.user, 'users:list')
|
auth.verify_privilege(ctx.user, 'users:list')
|
||||||
return self._search_executor.execute_and_serialize(
|
return self._search_executor.execute_and_serialize(
|
||||||
ctx, lambda user: _serialize_user(ctx.user, user), 'users')
|
ctx, lambda user: serialize_user(user, ctx.user), 'users')
|
||||||
|
|
||||||
def post(self, ctx):
|
def post(self, ctx):
|
||||||
auth.verify_privilege(ctx.user, 'users:create')
|
auth.verify_privilege(ctx.user, 'users:create')
|
||||||
|
@ -58,7 +61,7 @@ class UserListApi(BaseApi):
|
||||||
|
|
||||||
ctx.session.add(user)
|
ctx.session.add(user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
return {'user': _serialize_user(ctx.user, user)}
|
return {'user': serialize_user(user, ctx.user)}
|
||||||
|
|
||||||
class UserDetailApi(BaseApi):
|
class UserDetailApi(BaseApi):
|
||||||
def get(self, ctx, user_name):
|
def get(self, ctx, user_name):
|
||||||
|
@ -66,7 +69,7 @@ class UserDetailApi(BaseApi):
|
||||||
user = users.get_user_by_name(user_name)
|
user = users.get_user_by_name(user_name)
|
||||||
if not user:
|
if not user:
|
||||||
raise users.UserNotFoundError('User %r not found.' % user_name)
|
raise users.UserNotFoundError('User %r not found.' % user_name)
|
||||||
return {'user': _serialize_user(ctx.user, user)}
|
return {'user': serialize_user(user, ctx.user)}
|
||||||
|
|
||||||
def put(self, ctx, user_name):
|
def put(self, ctx, user_name):
|
||||||
user = users.get_user_by_name(user_name)
|
user = users.get_user_by_name(user_name)
|
||||||
|
@ -102,7 +105,7 @@ class UserDetailApi(BaseApi):
|
||||||
ctx.get_file('avatar'))
|
ctx.get_file('avatar'))
|
||||||
|
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
return {'user': _serialize_user(ctx.user, user)}
|
return {'user': serialize_user(user, ctx.user)}
|
||||||
|
|
||||||
def delete(self, ctx, user_name):
|
def delete(self, ctx, user_name):
|
||||||
user = users.get_user_by_name(user_name)
|
user = users.get_user_by_name(user_name)
|
||||||
|
|
|
@ -55,6 +55,7 @@ def create_app():
|
||||||
tag_detail_api = api.TagDetailApi()
|
tag_detail_api = api.TagDetailApi()
|
||||||
tag_merge_api = api.TagMergeApi()
|
tag_merge_api = api.TagMergeApi()
|
||||||
tag_siblings_api = api.TagSiblingsApi()
|
tag_siblings_api = api.TagSiblingsApi()
|
||||||
|
post_feature_api = api.PostFeatureApi()
|
||||||
password_reset_api = api.PasswordResetApi()
|
password_reset_api = api.PasswordResetApi()
|
||||||
snapshot_list_api = api.SnapshotListApi()
|
snapshot_list_api = api.SnapshotListApi()
|
||||||
info_api = api.InfoApi()
|
info_api = api.InfoApi()
|
||||||
|
@ -77,5 +78,6 @@ def create_app():
|
||||||
app.add_route('/password-reset/{user_name}', password_reset_api)
|
app.add_route('/password-reset/{user_name}', password_reset_api)
|
||||||
app.add_route('/snapshots/', snapshot_list_api)
|
app.add_route('/snapshots/', snapshot_list_api)
|
||||||
app.add_route('/info/', info_api)
|
app.add_route('/info/', info_api)
|
||||||
|
app.add_route('/featured-post/', post_feature_api)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from sqlalchemy import Column, Integer, DateTime, String, Text, PickleType, ForeignKey
|
from sqlalchemy import Column, Integer, DateTime, String, Text, PickleType, ForeignKey
|
||||||
from sqlalchemy.orm import relationship, column_property
|
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
|
||||||
|
|
||||||
|
@ -87,9 +87,9 @@ class Post(Base):
|
||||||
checksum = Column('checksum', String(64), nullable=False)
|
checksum = Column('checksum', String(64), nullable=False)
|
||||||
source = Column('source', String(200))
|
source = Column('source', String(200))
|
||||||
file_size = Column('file_size', Integer)
|
file_size = Column('file_size', Integer)
|
||||||
image_width = Column('image_width', Integer)
|
canvas_width = Column('image_width', Integer)
|
||||||
image_height = Column('image_height', Integer)
|
canvas_height = Column('image_height', Integer)
|
||||||
flags = Column('flags', Integer, nullable=False, default=0)
|
flags = Column('flags', PickleType, default=None)
|
||||||
|
|
||||||
user = relationship('User')
|
user = relationship('User')
|
||||||
tags = relationship('Tag', backref='posts', secondary='post_tag')
|
tags = relationship('Tag', backref='posts', secondary='post_tag')
|
||||||
|
@ -102,7 +102,7 @@ class Post(Base):
|
||||||
'PostFeature', cascade='all, delete-orphan', lazy='joined')
|
'PostFeature', cascade='all, delete-orphan', lazy='joined')
|
||||||
scores = relationship(
|
scores = relationship(
|
||||||
'PostScore', cascade='all, delete-orphan', lazy='joined')
|
'PostScore', cascade='all, delete-orphan', lazy='joined')
|
||||||
favorites = relationship(
|
favorited_by = relationship(
|
||||||
'PostFavorite', cascade='all, delete-orphan', lazy='joined')
|
'PostFavorite', cascade='all, delete-orphan', lazy='joined')
|
||||||
notes = relationship(
|
notes = relationship(
|
||||||
'PostNote', cascade='all, delete-orphan', lazy='joined')
|
'PostNote', cascade='all, delete-orphan', lazy='joined')
|
||||||
|
@ -112,8 +112,16 @@ class Post(Base):
|
||||||
.where(PostTag.post_id == post_id) \
|
.where(PostTag.post_id == post_id) \
|
||||||
.correlate_except(PostTag))
|
.correlate_except(PostTag))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_featured(self):
|
||||||
|
featured_post = object_session(self) \
|
||||||
|
.query(PostFeature) \
|
||||||
|
.order_by(PostFeature.time.desc()) \
|
||||||
|
.first()
|
||||||
|
return featured_post and featured_post.post_id == self.post_id
|
||||||
|
|
||||||
# TODO: wire these
|
# TODO: wire these
|
||||||
fav_count = Column('auto_fav_count', Integer, nullable=False, default=0)
|
favorite_count = Column('auto_fav_count', Integer, nullable=False, default=0)
|
||||||
score = Column('auto_score', Integer, nullable=False, default=0)
|
score = Column('auto_score', Integer, nullable=False, default=0)
|
||||||
feature_count = Column('auto_feature_count', Integer, nullable=False, default=0)
|
feature_count = Column('auto_feature_count', Integer, nullable=False, default=0)
|
||||||
comment_count = Column('auto_comment_count', Integer, nullable=False, default=0)
|
comment_count = Column('auto_comment_count', Integer, nullable=False, default=0)
|
||||||
|
|
|
@ -1,5 +1,28 @@
|
||||||
|
import datetime
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from szurubooru import db
|
from szurubooru import db, errors
|
||||||
|
|
||||||
|
class PostNotFoundError(errors.NotFoundError): pass
|
||||||
|
class PostAlreadyFeaturedError(errors.ValidationError): pass
|
||||||
|
|
||||||
def get_post_count():
|
def get_post_count():
|
||||||
return db.session.query(sqlalchemy.func.count(db.Post.post_id)).one()[0]
|
return db.session.query(sqlalchemy.func.count(db.Post.post_id)).one()[0]
|
||||||
|
|
||||||
|
def get_post_by_id(post_id):
|
||||||
|
return db.session.query(db.Post) \
|
||||||
|
.filter(db.Post.post_id == post_id) \
|
||||||
|
.one_or_none()
|
||||||
|
|
||||||
|
def get_featured_post():
|
||||||
|
post_feature = db.session \
|
||||||
|
.query(db.PostFeature) \
|
||||||
|
.order_by(db.PostFeature.time.desc()) \
|
||||||
|
.first()
|
||||||
|
return post_feature.post if post_feature else None
|
||||||
|
|
||||||
|
def feature_post(post, user):
|
||||||
|
post_feature = db.PostFeature()
|
||||||
|
post_feature.time = datetime.datetime.now()
|
||||||
|
post_feature.post = post
|
||||||
|
post_feature.user = user
|
||||||
|
db.session.add(post_feature)
|
||||||
|
|
|
@ -3,13 +3,27 @@ from sqlalchemy.inspection import inspect
|
||||||
from szurubooru import db
|
from szurubooru import db
|
||||||
|
|
||||||
def get_tag_snapshot(tag):
|
def get_tag_snapshot(tag):
|
||||||
ret = {
|
return {
|
||||||
'names': [tag_name.name for tag_name in tag.names],
|
'names': [tag_name.name for tag_name in tag.names],
|
||||||
'category': tag.category.name,
|
'category': tag.category.name,
|
||||||
'suggestions': sorted(rel.first_name for rel in tag.suggestions),
|
'suggestions': sorted(rel.first_name for rel in tag.suggestions),
|
||||||
'implications': sorted(rel.first_name for rel in tag.implications),
|
'implications': sorted(rel.first_name for rel in tag.implications),
|
||||||
}
|
}
|
||||||
return ret
|
|
||||||
|
def get_post_snapshot(post):
|
||||||
|
return {
|
||||||
|
'source': post.source,
|
||||||
|
'safety': post.safety,
|
||||||
|
'checksum': post.checksum,
|
||||||
|
'tags': sorted([tag.first_name for tag in post.tags]),
|
||||||
|
'relations': sorted([rel.post_id for rel in post.relations]),
|
||||||
|
'notes': sorted([{
|
||||||
|
'polygon': note.polygon,
|
||||||
|
'text': note.text,
|
||||||
|
} for note in post.notes]),
|
||||||
|
'flags': post.flags,
|
||||||
|
'featured': post.is_featured,
|
||||||
|
}
|
||||||
|
|
||||||
def get_tag_category_snapshot(category):
|
def get_tag_category_snapshot(category):
|
||||||
return {
|
return {
|
||||||
|
@ -25,6 +39,9 @@ serializers = {
|
||||||
'tag_category': (
|
'tag_category': (
|
||||||
get_tag_category_snapshot,
|
get_tag_category_snapshot,
|
||||||
lambda category: category.name),
|
lambda category: category.name),
|
||||||
|
'post': (
|
||||||
|
get_post_snapshot,
|
||||||
|
lambda post: post.post_id),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_resource_info(entity):
|
def get_resource_info(entity):
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
'''
|
||||||
|
Change flags column type
|
||||||
|
|
||||||
|
Revision ID: 84bd402f15f0
|
||||||
|
Created at: 2016-04-22 20:48:32.386159
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = '84bd402f15f0'
|
||||||
|
down_revision = '9587de88a84b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.drop_column('post', 'flags')
|
||||||
|
op.add_column('post', sa.Column('flags', sa.PickleType(), nullable=True))
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('post', 'flags')
|
||||||
|
op.add_column('post', sa.Column('flags', sa.Integer(), autoincrement=False, nullable=False))
|
82
server/szurubooru/tests/api/test_post_featuring.py
Normal file
82
server/szurubooru/tests/api/test_post_featuring.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import datetime
|
||||||
|
import pytest
|
||||||
|
from szurubooru import api, db, errors
|
||||||
|
from szurubooru.func import util, posts
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_ctx(context_factory, config_injector, user_factory, post_factory):
|
||||||
|
config_injector({
|
||||||
|
'privileges': {'posts:feature': 'regular_user'},
|
||||||
|
'ranks': ['anonymous', 'regular_user'],
|
||||||
|
})
|
||||||
|
ret = util.dotdict()
|
||||||
|
ret.context_factory = context_factory
|
||||||
|
ret.user_factory = user_factory
|
||||||
|
ret.post_factory = post_factory
|
||||||
|
ret.api = api.PostFeatureApi()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def test_featuring(test_ctx):
|
||||||
|
db.session.add(test_ctx.post_factory(id=1))
|
||||||
|
db.session.commit()
|
||||||
|
assert posts.get_featured_post() is None
|
||||||
|
assert not posts.get_post_by_id(1).is_featured
|
||||||
|
result = test_ctx.api.post(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'id': 1},
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
assert posts.get_featured_post() is not None
|
||||||
|
assert posts.get_featured_post().post_id == 1
|
||||||
|
assert posts.get_post_by_id(1).is_featured
|
||||||
|
assert 'post' in result
|
||||||
|
assert 'snapshots' in result
|
||||||
|
assert 'id' in result['post']
|
||||||
|
|
||||||
|
def test_trying_to_feature_the_same_post_twice(test_ctx):
|
||||||
|
db.session.add(test_ctx.post_factory(id=1))
|
||||||
|
db.session.commit()
|
||||||
|
test_ctx.api.post(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'id': 1},
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
with pytest.raises(posts.PostAlreadyFeaturedError):
|
||||||
|
test_ctx.api.post(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'id': 1},
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
|
||||||
|
def test_featuring_one_post_after_another(test_ctx, fake_datetime):
|
||||||
|
db.session.add(test_ctx.post_factory(id=1))
|
||||||
|
db.session.add(test_ctx.post_factory(id=2))
|
||||||
|
db.session.commit()
|
||||||
|
assert posts.get_featured_post() is None
|
||||||
|
assert not posts.get_post_by_id(1).is_featured
|
||||||
|
assert not posts.get_post_by_id(2).is_featured
|
||||||
|
with fake_datetime('1997'):
|
||||||
|
result = test_ctx.api.post(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'id': 1},
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
with fake_datetime('1998'):
|
||||||
|
result = test_ctx.api.post(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'id': 2},
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
assert posts.get_featured_post() is not None
|
||||||
|
assert posts.get_featured_post().post_id == 2
|
||||||
|
assert not posts.get_post_by_id(1).is_featured
|
||||||
|
assert posts.get_post_by_id(2).is_featured
|
||||||
|
|
||||||
|
def test_trying_to_feature_non_existing(test_ctx):
|
||||||
|
with pytest.raises(posts.PostNotFoundError):
|
||||||
|
test_ctx.api.post(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'id': 1},
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
|
||||||
|
def test_trying_to_feature_without_privileges(test_ctx):
|
||||||
|
with pytest.raises(errors.AuthError):
|
||||||
|
test_ctx.api.post(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'id': 1},
|
||||||
|
user=test_ctx.user_factory(rank='anonymous')))
|
|
@ -125,14 +125,16 @@ def tag_factory(session):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def post_factory():
|
def post_factory():
|
||||||
def factory(
|
def factory(
|
||||||
|
id=None,
|
||||||
safety=db.Post.SAFETY_SAFE,
|
safety=db.Post.SAFETY_SAFE,
|
||||||
type=db.Post.TYPE_IMAGE,
|
type=db.Post.TYPE_IMAGE,
|
||||||
checksum='...'):
|
checksum='...'):
|
||||||
post = db.Post()
|
post = db.Post()
|
||||||
|
post.post_id = id
|
||||||
post.safety = safety
|
post.safety = safety
|
||||||
post.type = type
|
post.type = type
|
||||||
post.checksum = checksum
|
post.checksum = checksum
|
||||||
post.flags = 0
|
post.flags = []
|
||||||
post.creation_time = datetime.datetime(1996, 1, 1)
|
post.creation_time = datetime.datetime(1996, 1, 1)
|
||||||
return post
|
return post
|
||||||
return factory
|
return factory
|
||||||
|
|
|
@ -68,7 +68,7 @@ def test_cascade_deletions(post_factory, user_factory, tag_factory):
|
||||||
post.relations.append(related_post1)
|
post.relations.append(related_post1)
|
||||||
post.relations.append(related_post2)
|
post.relations.append(related_post2)
|
||||||
post.scores.append(score)
|
post.scores.append(score)
|
||||||
post.favorites.append(favorite)
|
post.favorited_by.append(favorite)
|
||||||
post.features.append(feature)
|
post.features.append(feature)
|
||||||
post.notes.append(note)
|
post.notes.append(note)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
|
@ -3,6 +3,65 @@ import pytest
|
||||||
from szurubooru import db
|
from szurubooru import db
|
||||||
from szurubooru.func import snapshots
|
from szurubooru.func import snapshots
|
||||||
|
|
||||||
|
def test_serializing_post(post_factory, user_factory, tag_factory):
|
||||||
|
user = user_factory(name='dummy-user')
|
||||||
|
tag1 = tag_factory(names=['dummy-tag1'])
|
||||||
|
tag2 = tag_factory(names=['dummy-tag2'])
|
||||||
|
post = post_factory(id=1)
|
||||||
|
related_post1 = post_factory(id=2)
|
||||||
|
related_post2 = post_factory(id=3)
|
||||||
|
db.session.add_all([user, tag1, tag2, post, related_post1, related_post2])
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
score = db.PostScore()
|
||||||
|
score.post = post
|
||||||
|
score.user = user
|
||||||
|
score.time = datetime.datetime(1997, 1, 1)
|
||||||
|
score.score = 1
|
||||||
|
favorite = db.PostFavorite()
|
||||||
|
favorite.post = post
|
||||||
|
favorite.user = user
|
||||||
|
favorite.time = datetime.datetime(1997, 1, 1)
|
||||||
|
feature = db.PostFeature()
|
||||||
|
feature.post = post
|
||||||
|
feature.user = user
|
||||||
|
feature.time = datetime.datetime(1997, 1, 1)
|
||||||
|
note = db.PostNote()
|
||||||
|
note.post = post
|
||||||
|
note.polygon = [(1, 1), (200, 1), (200, 200), (1, 200)]
|
||||||
|
note.text = 'some text'
|
||||||
|
db.session.add_all([score])
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
post.user = user
|
||||||
|
post.checksum = 'deadbeef'
|
||||||
|
post.source = 'example.com'
|
||||||
|
post.tags.append(tag1)
|
||||||
|
post.tags.append(tag2)
|
||||||
|
post.relations.append(related_post1)
|
||||||
|
post.relations.append(related_post2)
|
||||||
|
post.scores.append(score)
|
||||||
|
post.favorited_by.append(favorite)
|
||||||
|
post.features.append(feature)
|
||||||
|
post.notes.append(note)
|
||||||
|
|
||||||
|
assert snapshots.get_post_snapshot(post) == {
|
||||||
|
'checksum': 'deadbeef',
|
||||||
|
'featured': True,
|
||||||
|
'flags': [],
|
||||||
|
'notes': [
|
||||||
|
{
|
||||||
|
'polygon': [(1, 1), (200, 1), (200, 200), (1, 200)],
|
||||||
|
'text': 'some text',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'relations': [2, 3],
|
||||||
|
'safety': 'safe',
|
||||||
|
'source': 'example.com',
|
||||||
|
'tags': ['dummy-tag1', 'dummy-tag2'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_serializing_tag(tag_factory):
|
def test_serializing_tag(tag_factory):
|
||||||
tag = tag_factory(names=['main_name', 'alias'], category_name='dummy')
|
tag = tag_factory(names=['main_name', 'alias'], category_name='dummy')
|
||||||
assert snapshots.get_tag_snapshot(tag) == {
|
assert snapshots.get_tag_snapshot(tag) == {
|
||||||
|
|
Loading…
Reference in a new issue