server/comments+posts: add rating

This commit is contained in:
rr- 2016-04-24 16:34:06 +02:00
parent 8fb536c8f0
commit 52f4018bee
16 changed files with 602 additions and 84 deletions

84
API.md
View File

@ -33,7 +33,7 @@
- ~~Updating post~~ - ~~Updating post~~
- ~~Getting post~~ - ~~Getting post~~
- ~~Deleting post~~ - ~~Deleting post~~
- ~~Rating post~~ - [Rating post](#rating-post)
- ~~Adding post to favorites~~ - ~~Adding post to favorites~~
- ~~Removing post from favorites~~ - ~~Removing post from favorites~~
- [Getting featured post](#getting-featured-post) - [Getting featured post](#getting-featured-post)
@ -44,7 +44,7 @@
- [Updating comment](#updating-comment) - [Updating comment](#updating-comment)
- [Getting comment](#getting-comment) - [Getting comment](#getting-comment)
- [Deleting comment](#deleting-comment) - [Deleting comment](#deleting-comment)
- ~~Rating comment~~ - [Rating comment](#rating-comment)
- Users - Users
- [Listing users](#listing-users) - [Listing users](#listing-users)
- [Creating user](#creating-user) - [Creating user](#creating-user)
@ -612,6 +612,40 @@ 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.
## Rating post
- **Request**
`PUT /post/<id>/score`
- **Input**
```json5
{
"score": <score>
}
```
- **Output**
```json5
{
"post": <post>
}
```
...where `<post>` is a [post resource](#post).
- **Errors**
- post does not exist
- score is invalid
- privileges are too low
- **Description**
Updates score of authenticated user for given post. Valid scores are -1, 0
and 1.
## Getting featured post ## Getting featured post
- **Request** - **Request**
@ -854,6 +888,40 @@ data.
Deletes existing comment. Deletes existing comment.
## Rating comment
- **Request**
`PUT /comment/<id>/score`
- **Input**
```json5
{
"score": <score>
}
```
- **Output**
```json5
{
"comment": <comment>
}
```
...where `<comment>` is a [comment resource](#comment).
- **Errors**
- comment does not exist
- score is invalid
- privileges are too low
- **Description**
Updates score of authenticated user for given comment. Valid scores are -1,
0 and 1.
## Listing users ## Listing users
- **Request** - **Request**
@ -1317,6 +1385,7 @@ One file together with its metadata posted to the site.
"lastEditTime": <last-edit-time>, "lastEditTime": <last-edit-time>,
"user": <user>, "user": <user>,
"score": <score>, "score": <score>,
"ownScore": <own-score>,
"favoritedBy": <favorited-by>, "favoritedBy": <favorited-by>,
"featureCount": <feature-count>, "featureCount": <feature-count>,
"lastFeatureTime": <last-feature-time>, "lastFeatureTime": <last-feature-time>,
@ -1357,7 +1426,9 @@ One file together with its metadata posted to the site.
- `<creation-time>`: time the tag was created, formatted as per RFC 3339. - `<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. - `<last-edit-time>`: time the tag was edited, formatted as per RFC 3339.
- `<user>`: who created the post, serialized as [user resource](#user). - `<user>`: who created the post, serialized as [user resource](#user).
- `<score>`: the score (+1/-1 rating) of the given post. - `<score>`: the collective score (+1/-1 rating) of the given post.
- `<own-score>`: the score (+1/-1 rating) of the given post by the
authenticated user.
- `<favorited-by>`: list of users, serialized as [user resources](#user). - `<favorited-by>`: list of users, serialized as [user resources](#user).
- `<feature-count>`: how many times has the post been featured. - `<feature-count>`: how many times has the post been featured.
- `<last-feature-time>`: the last time the post was featured, formatted as per - `<last-feature-time>`: the last time the post was featured, formatted as per
@ -1377,7 +1448,9 @@ A comment under a post.
"user": <author> "user": <author>
"text": <text>, "text": <text>,
"creationTime": <creation-time>, "creationTime": <creation-time>,
"lastEditTime": <last-edit-time> "lastEditTime": <last-edit-time>,
"score": <score>,
"ownScore": <own-score>
} }
``` ```
@ -1387,6 +1460,9 @@ A comment under a post.
- `<author>`: a user resource the post is created by. - `<author>`: a user resource the post is created by.
- `<creation-time>`: time the comment was created, formatted as per RFC 3339. - `<creation-time>`: time the comment was created, formatted as per RFC 3339.
- `<last-edit-time>`: time the comment was edited, formatted as per RFC 3339. - `<last-edit-time>`: time the comment was edited, formatted as per RFC 3339.
- `<score>`: the collective score (+1/-1 rating) of the given comment.
- `<own-score>`: the score (+1/-1 rating) of the given comment by the
authenticated user.
## Snapshot ## Snapshot

View File

@ -93,6 +93,7 @@ privileges:
'posts:edit:thumbnail': power_user 'posts:edit:thumbnail': power_user
'posts:feature': mod 'posts:feature': mod
'posts:delete': mod 'posts:delete': mod
'posts:score': regular_user
'tags:create': regular_user 'tags:create': regular_user
'tags:edit:names': power_user 'tags:edit:names': power_user
@ -119,5 +120,6 @@ privileges:
'comments:edit:own': regular_user 'comments:edit:own': regular_user
'comments:list': regular_user 'comments:list': regular_user
'comments:view': regular_user 'comments:view': regular_user
'comments:score': regular_user
'snapshots:list': power_user 'snapshots:list': power_user

View File

@ -12,8 +12,11 @@ from szurubooru.api.tag_category_api import (
TagCategoryDetailApi) TagCategoryDetailApi)
from szurubooru.api.comment_api import ( from szurubooru.api.comment_api import (
CommentListApi, CommentListApi,
CommentDetailApi) CommentDetailApi,
from szurubooru.api.post_api import PostFeatureApi CommentScoreApi)
from szurubooru.api.post_api import (
PostFeatureApi,
PostScoreApi)
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

View File

@ -1,7 +1,7 @@
import datetime import datetime
from szurubooru import search from szurubooru import search
from szurubooru.api.base_api import BaseApi from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, comments, posts from szurubooru.func import auth, comments, posts, scores
class CommentListApi(BaseApi): class CommentListApi(BaseApi):
def __init__(self): def __init__(self):
@ -49,3 +49,19 @@ class CommentDetailApi(BaseApi):
ctx.session.delete(comment) ctx.session.delete(comment)
ctx.session.commit() ctx.session.commit()
return {} return {}
class CommentScoreApi(BaseApi):
def put(self, ctx, comment_id):
auth.verify_privilege(ctx.user, 'comments:score')
score = ctx.get_param_as_int('score', required=True)
comment = comments.get_comment_by_id(comment_id)
scores.set_score(comment, ctx.user, score)
ctx.session.commit()
return {'comment': comments.serialize_comment(comment, ctx.user)}
def delete(self, ctx, comment_id):
auth.verify_privilege(ctx.user, 'comments:score')
comment = comments.get_comment_by_id(comment_id)
scores.delete_score(comment, ctx.user)
ctx.session.commit()
return {'comment': comments.serialize_comment(comment, ctx.user)}

View File

@ -1,5 +1,5 @@
from szurubooru.api.base_api import BaseApi from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, posts, snapshots from szurubooru.func import auth, posts, snapshots, scores
class PostFeatureApi(BaseApi): class PostFeatureApi(BaseApi):
def post(self, ctx): def post(self, ctx):
@ -20,3 +20,19 @@ class PostFeatureApi(BaseApi):
def get(self, ctx): def get(self, ctx):
post = posts.try_get_featured_post() post = posts.try_get_featured_post()
return posts.serialize_post_with_details(post, ctx.user) return posts.serialize_post_with_details(post, ctx.user)
class PostScoreApi(BaseApi):
def put(self, ctx, post_id):
auth.verify_privilege(ctx.user, 'posts:score')
post = posts.get_post_by_id(post_id)
score = ctx.get_param_as_int('score', required=True)
scores.set_score(post, ctx.user, score)
ctx.session.commit()
return {'post': posts.serialize_post(post, ctx.user)}
def delete(self, ctx, post_id):
auth.verify_privilege(ctx.user, 'posts:score')
post = posts.get_post_by_id(post_id)
scores.delete_score(post, ctx.user)
ctx.session.commit()
return {'post': posts.serialize_post(post, ctx.user)}

View File

@ -47,21 +47,6 @@ def create_app():
middleware.Authenticator(), middleware.Authenticator(),
]) ])
user_list_api = api.UserListApi()
user_detail_api = api.UserDetailApi()
tag_category_list_api = api.TagCategoryListApi()
tag_category_detail_api = api.TagCategoryDetailApi()
tag_list_api = api.TagListApi()
tag_detail_api = api.TagDetailApi()
tag_merge_api = api.TagMergeApi()
tag_siblings_api = api.TagSiblingsApi()
post_feature_api = api.PostFeatureApi()
password_reset_api = api.PasswordResetApi()
snapshot_list_api = api.SnapshotListApi()
comment_list_api = api.CommentListApi()
comment_detail_api = api.CommentDetailApi()
info_api = api.InfoApi()
app.add_error_handler(errors.AuthError, _on_auth_error) app.add_error_handler(errors.AuthError, _on_auth_error)
app.add_error_handler(errors.IntegrityError, _on_integrity_error) app.add_error_handler(errors.IntegrityError, _on_integrity_error)
app.add_error_handler(errors.ValidationError, _on_validation_error) app.add_error_handler(errors.ValidationError, _on_validation_error)
@ -69,19 +54,25 @@ def create_app():
app.add_error_handler(errors.NotFoundError, _on_not_found_error) app.add_error_handler(errors.NotFoundError, _on_not_found_error)
app.add_error_handler(errors.ProcessingError, _on_processing_error) app.add_error_handler(errors.ProcessingError, _on_processing_error)
app.add_route('/users/', user_list_api) app.add_route('/users/', api.UserListApi())
app.add_route('/user/{user_name}', user_detail_api) app.add_route('/user/{user_name}', api.UserDetailApi())
app.add_route('/tag-categories/', tag_category_list_api) app.add_route('/password-reset/{user_name}', api.PasswordResetApi())
app.add_route('/tag-category/{category_name}', tag_category_detail_api)
app.add_route('/tags/', tag_list_api) app.add_route('/tag-categories/', api.TagCategoryListApi())
app.add_route('/tag/{tag_name}', tag_detail_api) app.add_route('/tag-category/{category_name}', api.TagCategoryDetailApi())
app.add_route('/tag-merge/', tag_merge_api) app.add_route('/tags/', api.TagListApi())
app.add_route('/tag-siblings/{tag_name}', tag_siblings_api) app.add_route('/tag/{tag_name}', api.TagDetailApi())
app.add_route('/password-reset/{user_name}', password_reset_api) app.add_route('/tag-merge/', api.TagMergeApi())
app.add_route('/snapshots/', snapshot_list_api) app.add_route('/tag-siblings/{tag_name}', api.TagSiblingsApi())
app.add_route('/info/', info_api)
app.add_route('/featured-post/', post_feature_api) app.add_route('/post/{post_id}/score', api.PostScoreApi())
app.add_route('/comments/', comment_list_api)
app.add_route('/comment/{comment_id}', comment_detail_api) app.add_route('/comments/', api.CommentListApi())
app.add_route('/comment/{comment_id}', api.CommentDetailApi())
app.add_route('/comment/{comment_id}/score', api.CommentScoreApi())
app.add_route('/info/', api.InfoApi())
app.add_route('/featured-post/', api.PostFeatureApi())
app.add_route('/snapshots/', api.SnapshotListApi())
return app return app

View File

@ -1,5 +1,6 @@
from sqlalchemy import Column, Integer, DateTime, Text, ForeignKey from sqlalchemy import Column, Integer, DateTime, Text, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship, object_session
from sqlalchemy.sql.expression import func
from szurubooru.db.base import Base from szurubooru.db.base import Base
class CommentScore(Base): class CommentScore(Base):
@ -27,3 +28,10 @@ class Comment(Base):
post = relationship('Post') post = relationship('Post')
scores = relationship( scores = relationship(
'CommentScore', cascade='all, delete-orphan', lazy='joined') 'CommentScore', cascade='all, delete-orphan', lazy='joined')
@property
def score(self):
return object_session(self) \
.query(func.sum(CommentScore.score)) \
.filter(CommentScore.comment_id == self.comment_id) \
.one()[0] or 0

View File

@ -120,17 +120,30 @@ 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
def score(self):
return object_session(self) \
.query(func.sum(PostScore.score)) \
.filter(PostScore.post_id == self.post_id) \
.one()[0] or 0
feature_count = column_property(
select([func.count(PostFeature.post_id)]) \
.where(PostFeature.post_id == post_id) \
.correlate_except(PostFeature))
last_feature_time = column_property(
select([func.max(PostFeature.time)]) \
.where(PostFeature.post_id == post_id) \
.correlate_except(PostFeature))
# TODO: wire these # TODO: wire these
favorite_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) #comment_count = Column('auto_comment_count', Integer, nullable=False, default=0)
feature_count = Column('auto_feature_count', Integer, nullable=False, default=0) #note_count = Column('auto_note_count', Integer, nullable=False, default=0)
comment_count = Column('auto_comment_count', Integer, nullable=False, default=0) #last_fav_time = Column(
note_count = Column('auto_note_count', Integer, nullable=False, default=0) # 'auto_fav_time', Integer, nullable=False, default=0)
last_fav_time = Column( #last_comment_edit_time = Column(
'auto_fav_time', Integer, nullable=False, default=0) # 'auto_comment_creation_time', Integer, nullable=False, default=0)
last_feature_time = Column( #last_comment_creation_time = Column(
'auto_feature_time', Integer, nullable=False, default=0) # 'auto_comment_edit_time', 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)

View File

@ -1,12 +1,12 @@
import datetime import datetime
from szurubooru import db, errors from szurubooru import db, errors
from szurubooru.func import users, posts from szurubooru.func import users, posts, scores
class CommentNotFoundError(errors.NotFoundError): pass class CommentNotFoundError(errors.NotFoundError): pass
class EmptyCommentTextError(errors.ValidationError): pass class EmptyCommentTextError(errors.ValidationError): pass
def serialize_comment(comment, authenticated_user): def serialize_comment(comment, authenticated_user):
return { ret = {
'id': comment.comment_id, 'id': comment.comment_id,
'user': users.serialize_user(comment.user, authenticated_user), 'user': users.serialize_user(comment.user, authenticated_user),
'post': posts.serialize_post(comment.post, authenticated_user), 'post': posts.serialize_post(comment.post, authenticated_user),
@ -14,6 +14,9 @@ def serialize_comment(comment, authenticated_user):
'creationTime': comment.creation_time, 'creationTime': comment.creation_time,
'lastEditTime': comment.last_edit_time, 'lastEditTime': comment.last_edit_time,
} }
if authenticated_user:
ret['ownScore'] = scores.get_score(comment, authenticated_user)
return ret
def try_get_comment_by_id(comment_id): def try_get_comment_by_id(comment_id):
return db.session \ return db.session \

View File

@ -1,7 +1,7 @@
import datetime import datetime
import sqlalchemy import sqlalchemy
from szurubooru import db, errors from szurubooru import db, errors
from szurubooru.func import users, snapshots from szurubooru.func import users, snapshots, scores
class PostNotFoundError(errors.NotFoundError): pass class PostNotFoundError(errors.NotFoundError): pass
class PostAlreadyFeaturedError(errors.ValidationError): pass class PostAlreadyFeaturedError(errors.ValidationError): pass
@ -36,7 +36,8 @@ def serialize_post(post, authenticated_user):
for rel in post.favorited_by], for rel in post.favorited_by],
} }
# TODO: fetch own score if needed if authenticated_user:
ret['ownScore'] = scores.get_score(post, authenticated_user)
return ret return ret

View File

@ -0,0 +1,53 @@
import datetime
from szurubooru import db, errors
from szurubooru.func import util
class InvalidScoreError(errors.ValidationError): pass
def _get_table_info(entity):
resource_type, _, _ = util.get_resource_info(entity)
if resource_type == 'post':
return db.PostScore, lambda table: table.post_id
elif resource_type == 'comment':
return db.CommentScore, lambda table: table.comment_id
else:
assert False
def _get_score_entity(entity, user):
table, get_column = _get_table_info(entity)
return db.session \
.query(table) \
.filter(get_column(table) == get_column(entity)) \
.filter(table.user_id == user.user_id) \
.one_or_none()
def delete_score(entity, user):
score_entity = _get_score_entity(entity, user)
if score_entity:
db.session.delete(score_entity)
def get_score(entity, user):
score_entity = _get_score_entity(entity, user)
if score_entity:
return score_entity.score
else:
return 0
def set_score(entity, user, score):
if not score:
delete_score(entity, user)
return
if score not in (-1, 1):
raise InvalidScoreError(
'Score %r is invalid. Valid scores: %r.' % (score, (-1, 1)))
score_entity = _get_score_entity(entity, user)
if score_entity:
score_entity.score = score
else:
table, get_column = _get_table_info(entity)
score_entity = table()
setattr(score_entity, get_column(table).name, get_column(entity))
score_entity.score = score
score_entity.user = user
score_entity.time = datetime.datetime.now()
db.session.add(score_entity)

View File

@ -1,6 +1,6 @@
import datetime import datetime
from sqlalchemy.inspection import inspect
from szurubooru import db from szurubooru import db
from szurubooru.func import util
def get_tag_snapshot(tag): def get_tag_snapshot(tag):
return { return {
@ -33,33 +33,11 @@ def get_tag_category_snapshot(category):
# pylint: disable=invalid-name # pylint: disable=invalid-name
serializers = { serializers = {
'tag': ( 'tag': get_tag_snapshot,
get_tag_snapshot, 'tag_category': get_tag_category_snapshot,
lambda tag: tag.first_name), 'post': get_post_snapshot,
'tag_category': (
get_tag_category_snapshot,
lambda category: category.name),
'post': (
get_post_snapshot,
lambda post: post.post_id),
} }
def get_resource_info(entity):
resource_type = entity.__table__.name
assert resource_type in serializers
primary_key = inspect(entity).identity
assert primary_key is not None
assert len(primary_key) == 1
resource_repr = serializers[resource_type][1](entity)
assert resource_repr
resource_id = primary_key[0]
assert resource_id
return (resource_type, resource_id, resource_repr)
def get_previous_snapshot(snapshot): def get_previous_snapshot(snapshot):
return db.session \ return db.session \
.query(db.Snapshot) \ .query(db.Snapshot) \
@ -71,7 +49,7 @@ def get_previous_snapshot(snapshot):
.first() .first()
def get_snapshots(entity): def get_snapshots(entity):
resource_type, resource_id, _ = get_resource_info(entity) resource_type, resource_id, _ = util.get_resource_info(entity)
return db.session \ return db.session \
.query(db.Snapshot) \ .query(db.Snapshot) \
.filter(db.Snapshot.resource_type == resource_type) \ .filter(db.Snapshot.resource_type == resource_type) \
@ -103,7 +81,7 @@ def get_serialized_history(entity):
return ret return ret
def save(operation, entity, auth_user): def save(operation, entity, auth_user):
resource_type, resource_id, resource_repr = get_resource_info(entity) resource_type, resource_id, resource_repr = util.get_resource_info(entity)
now = datetime.datetime.now() now = datetime.datetime.now()
snapshot = db.Snapshot() snapshot = db.Snapshot()
@ -112,7 +90,7 @@ def save(operation, entity, auth_user):
snapshot.resource_type = resource_type snapshot.resource_type = resource_type
snapshot.resource_id = resource_id snapshot.resource_id = resource_id
snapshot.resource_repr = resource_repr snapshot.resource_repr = resource_repr
snapshot.data = serializers[resource_type][0](entity) snapshot.data = serializers[resource_type](entity)
snapshot.user = auth_user snapshot.user = auth_user
earlier_snapshots = get_snapshots(entity) earlier_snapshots = get_snapshots(entity)

View File

@ -1,7 +1,31 @@
import datetime import datetime
import re import re
from sqlalchemy.inspection import inspect
from szurubooru.errors import ValidationError from szurubooru.errors import ValidationError
def get_resource_info(entity):
serializers = {
'tag': lambda tag: tag.first_name,
'tag_category': lambda category: category.name,
'comment': lambda comment: comment.comment_id,
'post': lambda post: post.post_id,
}
resource_type = entity.__table__.name
assert resource_type in serializers
primary_key = inspect(entity).identity
assert primary_key is not None
assert len(primary_key) == 1
resource_repr = serializers[resource_type](entity)
assert resource_repr
resource_id = primary_key[0]
assert resource_id
return (resource_type, resource_id, resource_repr)
def is_valid_email(email): def is_valid_email(email):
''' Return whether given email address is valid or empty. ''' ''' Return whether given email address is valid or empty. '''
return not email or re.match(r'^[^@]*@[^@]*\.[^@]*$', email) return not email or re.match(r'^[^@]*@[^@]*\.[^@]*$', email)

View File

@ -0,0 +1,36 @@
'''
Delete post columns
Revision ID: ed6dd16a30f3
Created at: 2016-04-24 16:29:25.309154
'''
import sqlalchemy as sa
from alembic import op
revision = 'ed6dd16a30f3'
down_revision = '46df355634dc'
branch_labels = None
depends_on = None
def upgrade():
op.drop_column('post', 'auto_comment_edit_time')
op.drop_column('post', 'auto_fav_count')
op.drop_column('post', 'auto_comment_creation_time')
op.drop_column('post', 'auto_feature_count')
op.drop_column('post', 'auto_comment_count')
op.drop_column('post', 'auto_score')
op.drop_column('post', 'auto_fav_time')
op.drop_column('post', 'auto_feature_time')
op.drop_column('post', 'auto_note_count')
def downgrade():
op.add_column('post', sa.Column('auto_note_count', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_feature_time', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_fav_time', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_score', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_comment_count', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_feature_count', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_comment_creation_time', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_fav_count', sa.INTEGER(), autoincrement=False, nullable=False))
op.add_column('post', sa.Column('auto_comment_edit_time', sa.INTEGER(), autoincrement=False, nullable=False))

View File

@ -0,0 +1,150 @@
import datetime
import pytest
from szurubooru import api, db, errors
from szurubooru.func import util, comments, scores
@pytest.fixture
def test_ctx(config_injector, context_factory, user_factory, comment_factory):
config_injector({
'ranks': ['anonymous', 'regular_user'],
'rank_names': {'anonymous': 'Peasant', 'regular_user': 'Lord'},
'privileges': {'comments:score': 'regular_user'},
'thumbnails': {'avatar_width': 200},
})
db.session.flush()
ret = util.dotdict()
ret.context_factory = context_factory
ret.user_factory = user_factory
ret.comment_factory = comment_factory
ret.api = api.CommentScoreApi()
return ret
def test_simple_rating(test_ctx, fake_datetime):
user = test_ctx.user_factory(rank='regular_user')
comment = test_ctx.comment_factory(user=user)
db.session.add(comment)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user),
comment.comment_id)
assert 'comment' in result
assert 'text' in result['comment']
comment = db.session.query(db.Comment).one()
assert db.session.query(db.CommentScore).count() == 1
assert comment is not None
assert comment.score == 1
def test_updating_rating(test_ctx, fake_datetime):
user = test_ctx.user_factory(rank='regular_user')
comment = test_ctx.comment_factory(user=user)
db.session.add(comment)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user),
comment.comment_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': -1}, user=user),
comment.comment_id)
comment = db.session.query(db.Comment).one()
assert db.session.query(db.CommentScore).count() == 1
assert comment.score == -1
def test_updating_rating_to_zero(test_ctx, fake_datetime):
user = test_ctx.user_factory(rank='regular_user')
comment = test_ctx.comment_factory(user=user)
db.session.add(comment)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user),
comment.comment_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 0}, user=user),
comment.comment_id)
comment = db.session.query(db.Comment).one()
assert db.session.query(db.CommentScore).count() == 0
assert comment.score == 0
def test_deleting_rating(test_ctx, fake_datetime):
user = test_ctx.user_factory(rank='regular_user')
comment = test_ctx.comment_factory(user=user)
db.session.add(comment)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user),
comment.comment_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.delete(
test_ctx.context_factory(user=user), comment.comment_id)
comment = db.session.query(db.Comment).one()
assert db.session.query(db.CommentScore).count() == 0
assert comment.score == 0
def test_ratings_from_multiple_users(test_ctx, fake_datetime):
user1 = test_ctx.user_factory(rank='regular_user')
user2 = test_ctx.user_factory(rank='regular_user')
comment = test_ctx.comment_factory()
db.session.add_all([user1, user2, comment])
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user1),
comment.comment_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': -1}, user=user2),
comment.comment_id)
comment = db.session.query(db.Comment).one()
assert db.session.query(db.CommentScore).count() == 2
assert comment.score == 0
@pytest.mark.parametrize('input,expected_exception', [
({'score': None}, errors.ValidationError),
({'score': ''}, errors.ValidationError),
({'score': -2}, scores.InvalidScoreError),
({'score': 2}, scores.InvalidScoreError),
({'score': [1]}, errors.ValidationError),
])
def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception):
user = test_ctx.user_factory()
comment = test_ctx.comment_factory(user=user)
db.session.add(comment)
db.session.commit()
with pytest.raises(expected_exception):
test_ctx.api.put(
test_ctx.context_factory(input=input, user=user),
comment.comment_id)
def test_trying_to_omit_mandatory_field(test_ctx):
user = test_ctx.user_factory()
comment = test_ctx.comment_factory(user=user)
db.session.add(comment)
db.session.commit()
with pytest.raises(errors.ValidationError):
test_ctx.api.put(
test_ctx.context_factory(input={}, user=user),
comment.comment_id)
def test_trying_to_update_non_existing(test_ctx):
with pytest.raises(comments.CommentNotFoundError):
test_ctx.api.put(
test_ctx.context_factory(
input={'score': 1},
user=test_ctx.user_factory(rank='regular_user')),
5)
def test_trying_to_rate_without_privileges(test_ctx):
comment = test_ctx.comment_factory()
db.session.add(comment)
db.session.commit()
with pytest.raises(errors.AuthError):
test_ctx.api.put(
test_ctx.context_factory(
input={'score': 1},
user=test_ctx.user_factory(rank='anonymous')),
comment.comment_id)

View File

@ -0,0 +1,148 @@
import datetime
import pytest
from szurubooru import api, db, errors
from szurubooru.func import util, posts, scores
@pytest.fixture
def test_ctx(config_injector, context_factory, user_factory, post_factory):
config_injector({
'ranks': ['anonymous', 'regular_user'],
'rank_names': {'anonymous': 'Peasant', 'regular_user': 'Lord'},
'privileges': {'posts:score': 'regular_user'},
'thumbnails': {'avatar_width': 200},
})
db.session.flush()
ret = util.dotdict()
ret.context_factory = context_factory
ret.user_factory = user_factory
ret.post_factory = post_factory
ret.api = api.PostScoreApi()
return ret
def test_simple_rating(test_ctx, fake_datetime):
post = test_ctx.post_factory()
db.session.add(post)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(
input={'score': 1}, user=test_ctx.user_factory()),
post.post_id)
assert 'post' in result
assert 'id' in result['post']
post = db.session.query(db.Post).one()
assert db.session.query(db.PostScore).count() == 1
assert post is not None
assert post.score == 1
def test_updating_rating(test_ctx, fake_datetime):
user = test_ctx.user_factory()
post = test_ctx.post_factory()
db.session.add(post)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user),
post.post_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': -1}, user=user),
post.post_id)
post = db.session.query(db.Post).one()
assert db.session.query(db.PostScore).count() == 1
assert post.score == -1
def test_updating_rating_to_zero(test_ctx, fake_datetime):
user = test_ctx.user_factory()
post = test_ctx.post_factory()
db.session.add(post)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user),
post.post_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 0}, user=user),
post.post_id)
post = db.session.query(db.Post).one()
assert db.session.query(db.PostScore).count() == 0
assert post.score == 0
def test_deleting_rating(test_ctx, fake_datetime):
user = test_ctx.user_factory()
post = test_ctx.post_factory()
db.session.add(post)
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user),
post.post_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.delete(
test_ctx.context_factory(user=user), post.post_id)
post = db.session.query(db.Post).one()
assert db.session.query(db.PostScore).count() == 0
assert post.score == 0
def test_ratings_from_multiple_users(test_ctx, fake_datetime):
user1 = test_ctx.user_factory()
user2 = test_ctx.user_factory()
post = test_ctx.post_factory()
db.session.add_all([user1, user2, post])
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': 1}, user=user1),
post.post_id)
with fake_datetime('1997-12-02'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'score': -1}, user=user2),
post.post_id)
post = db.session.query(db.Post).one()
assert db.session.query(db.PostScore).count() == 2
assert post.score == 0
@pytest.mark.parametrize('input,expected_exception', [
({'score': None}, errors.ValidationError),
({'score': ''}, errors.ValidationError),
({'score': -2}, scores.InvalidScoreError),
({'score': 2}, scores.InvalidScoreError),
({'score': [1]}, errors.ValidationError),
])
def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception):
post = test_ctx.post_factory()
db.session.add(post)
db.session.commit()
with pytest.raises(expected_exception):
test_ctx.api.put(
test_ctx.context_factory(input=input, user=test_ctx.user_factory()),
post.post_id)
def test_trying_to_omit_mandatory_field(test_ctx):
post = test_ctx.post_factory()
db.session.add(post)
db.session.commit()
with pytest.raises(errors.ValidationError):
test_ctx.api.put(
test_ctx.context_factory(input={}, user=test_ctx.user_factory()),
post.post_id)
def test_trying_to_update_non_existing(test_ctx):
with pytest.raises(posts.PostNotFoundError):
test_ctx.api.put(
test_ctx.context_factory(
input={'score': 1},
user=test_ctx.user_factory()),
5)
def test_trying_to_rate_without_privileges(test_ctx):
post = test_ctx.post_factory()
db.session.add(post)
db.session.commit()
with pytest.raises(errors.AuthError):
test_ctx.api.put(
test_ctx.context_factory(
input={'score': 1},
user=test_ctx.user_factory(rank='anonymous')),
post.post_id)