aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--API.md84
-rw-r--r--config.yaml.dist2
-rw-r--r--server/szurubooru/api/__init__.py7
-rw-r--r--server/szurubooru/api/comment_api.py18
-rw-r--r--server/szurubooru/api/post_api.py18
-rw-r--r--server/szurubooru/app.py49
-rw-r--r--server/szurubooru/db/comment.py10
-rw-r--r--server/szurubooru/db/post.py39
-rw-r--r--server/szurubooru/func/comments.py7
-rw-r--r--server/szurubooru/func/posts.py5
-rw-r--r--server/szurubooru/func/scores.py53
-rw-r--r--server/szurubooru/func/snapshots.py36
-rw-r--r--server/szurubooru/func/util.py24
-rw-r--r--server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py36
-rw-r--r--server/szurubooru/tests/api/test_comment_rating.py150
-rw-r--r--server/szurubooru/tests/api/test_post_rating.py148
16 files changed, 602 insertions, 84 deletions
diff --git a/API.md b/API.md
index fe097cc..460aec3 100644
--- a/API.md
+++ b/API.md
@@ -33,7 +33,7 @@
- ~~Updating post~~
- ~~Getting post~~
- ~~Deleting post~~
- - ~~Rating post~~
+ - [Rating post](#rating-post)
- ~~Adding post to favorites~~
- ~~Removing post from favorites~~
- [Getting featured post](#getting-featured-post)
@@ -44,7 +44,7 @@
- [Updating comment](#updating-comment)
- [Getting comment](#getting-comment)
- [Deleting comment](#deleting-comment)
- - ~~Rating comment~~
+ - [Rating comment](#rating-comment)
- Users
- [Listing users](#listing-users)
- [Creating user](#creating-user)
@@ -612,6 +612,40 @@ data.
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
- **Request**
@@ -854,6 +888,40 @@ data.
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
- **Request**
@@ -1317,6 +1385,7 @@ One file together with its metadata posted to the site.
"lastEditTime": <last-edit-time>,
"user": <user>,
"score": <score>,
+ "ownScore": <own-score>,
"favoritedBy": <favorited-by>,
"featureCount": <feature-count>,
"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.
- `<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.
+- `<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).
- `<feature-count>`: how many times has the post been featured.
- `<last-feature-time>`: the last time the post was featured, formatted as per
@@ -1377,7 +1448,9 @@ A comment under a post.
"user": <author>
"text": <text>,
"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.
- `<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.
+- `<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
diff --git a/config.yaml.dist b/config.yaml.dist
index dd74cd7..016fcd1 100644
--- a/config.yaml.dist
+++ b/config.yaml.dist
@@ -93,6 +93,7 @@ privileges:
'posts:edit:thumbnail': power_user
'posts:feature': mod
'posts:delete': mod
+ 'posts:score': regular_user
'tags:create': regular_user
'tags:edit:names': power_user
@@ -119,5 +120,6 @@ privileges:
'comments:edit:own': regular_user
'comments:list': regular_user
'comments:view': regular_user
+ 'comments:score': regular_user
'snapshots:list': power_user
diff --git a/server/szurubooru/api/__init__.py b/server/szurubooru/api/__init__.py
index f95f776..ac0b01f 100644
--- a/server/szurubooru/api/__init__.py
+++ b/server/szurubooru/api/__init__.py
@@ -12,8 +12,11 @@ from szurubooru.api.tag_category_api import (
TagCategoryDetailApi)
from szurubooru.api.comment_api import (
CommentListApi,
- CommentDetailApi)
-from szurubooru.api.post_api import PostFeatureApi
+ CommentDetailApi,
+ CommentScoreApi)
+from szurubooru.api.post_api import (
+ PostFeatureApi,
+ PostScoreApi)
from szurubooru.api.snapshot_api import SnapshotListApi
from szurubooru.api.info_api import InfoApi
from szurubooru.api.context import Context, Request
diff --git a/server/szurubooru/api/comment_api.py b/server/szurubooru/api/comment_api.py
index 7f675d1..c3afda1 100644
--- a/server/szurubooru/api/comment_api.py
+++ b/server/szurubooru/api/comment_api.py
@@ -1,7 +1,7 @@
import datetime
from szurubooru import search
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):
def __init__(self):
@@ -49,3 +49,19 @@ class CommentDetailApi(BaseApi):
ctx.session.delete(comment)
ctx.session.commit()
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)}
diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py
index 7565b92..0316ca3 100644
--- a/server/szurubooru/api/post_api.py
+++ b/server/szurubooru/api/post_api.py
@@ -1,5 +1,5 @@
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):
def post(self, ctx):
@@ -20,3 +20,19 @@ class PostFeatureApi(BaseApi):
def get(self, ctx):
post = posts.try_get_featured_post()
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)}
diff --git a/server/szurubooru/app.py b/server/szurubooru/app.py
index bae728e..236b7ac 100644
--- a/server/szurubooru/app.py
+++ b/server/szurubooru/app.py
@@ -47,21 +47,6 @@ def create_app():
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.IntegrityError, _on_integrity_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.ProcessingError, _on_processing_error)
- app.add_route('/users/', user_list_api)
- app.add_route('/user/{user_name}', user_detail_api)
- app.add_route('/tag-categories/', tag_category_list_api)
- app.add_route('/tag-category/{category_name}', tag_category_detail_api)
- app.add_route('/tags/', tag_list_api)
- app.add_route('/tag/{tag_name}', tag_detail_api)
- app.add_route('/tag-merge/', tag_merge_api)
- app.add_route('/tag-siblings/{tag_name}', tag_siblings_api)
- app.add_route('/password-reset/{user_name}', password_reset_api)
- app.add_route('/snapshots/', snapshot_list_api)
- app.add_route('/info/', info_api)
- app.add_route('/featured-post/', post_feature_api)
- app.add_route('/comments/', comment_list_api)
- app.add_route('/comment/{comment_id}', comment_detail_api)
+ app.add_route('/users/', api.UserListApi())
+ app.add_route('/user/{user_name}', api.UserDetailApi())
+ app.add_route('/password-reset/{user_name}', api.PasswordResetApi())
+
+ app.add_route('/tag-categories/', api.TagCategoryListApi())
+ app.add_route('/tag-category/{category_name}', api.TagCategoryDetailApi())
+ app.add_route('/tags/', api.TagListApi())
+ app.add_route('/tag/{tag_name}', api.TagDetailApi())
+ app.add_route('/tag-merge/', api.TagMergeApi())
+ app.add_route('/tag-siblings/{tag_name}', api.TagSiblingsApi())
+
+ app.add_route('/post/{post_id}/score', api.PostScoreApi())
+
+ 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
diff --git a/server/szurubooru/db/comment.py b/server/szurubooru/db/comment.py
index eedf181..c40e6e1 100644
--- a/server/szurubooru/db/comment.py
+++ b/server/szurubooru/db/comment.py
@@ -1,5 +1,6 @@
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
class CommentScore(Base):
@@ -27,3 +28,10 @@ class Comment(Base):
post = relationship('Post')
scores = relationship(
'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
diff --git a/server/szurubooru/db/post.py b/server/szurubooru/db/post.py
index 3c1972a..c09c92a 100644
--- a/server/szurubooru/db/post.py
+++ b/server/szurubooru/db/post.py
@@ -120,17 +120,30 @@ 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
+
+ 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
- favorite_count = Column('auto_fav_count', 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)
- comment_count = Column('auto_comment_count', Integer, nullable=False, default=0)
- note_count = Column('auto_note_count', Integer, nullable=False, default=0)
- last_fav_time = Column(
- 'auto_fav_time', Integer, nullable=False, default=0)
- last_feature_time = Column(
- 'auto_feature_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)
+ #favorite_count = Column('auto_fav_count', Integer, nullable=False, default=0)
+ #comment_count = Column('auto_comment_count', Integer, nullable=False, default=0)
+ #note_count = Column('auto_note_count', Integer, nullable=False, default=0)
+ #last_fav_time = Column(
+ # 'auto_fav_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)
diff --git a/server/szurubooru/func/comments.py b/server/szurubooru/func/comments.py
index 576cca2..edd7887 100644
--- a/server/szurubooru/func/comments.py
+++ b/server/szurubooru/func/comments.py
@@ -1,12 +1,12 @@
import datetime
from szurubooru import db, errors
-from szurubooru.func import users, posts
+from szurubooru.func import users, posts, scores
class CommentNotFoundError(errors.NotFoundError): pass
class EmptyCommentTextError(errors.ValidationError): pass
def serialize_comment(comment, authenticated_user):
- return {
+ ret = {
'id': comment.comment_id,
'user': users.serialize_user(comment.user, authenticated_user),
'post': posts.serialize_post(comment.post, authenticated_user),
@@ -14,6 +14,9 @@ def serialize_comment(comment, authenticated_user):
'creationTime': comment.creation_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):
return db.session \
diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py
index 5d947b2..f9ca093 100644
--- a/server/szurubooru/func/posts.py
+++ b/server/szurubooru/func/posts.py
@@ -1,7 +1,7 @@
import datetime
import sqlalchemy
from szurubooru import db, errors
-from szurubooru.func import users, snapshots
+from szurubooru.func import users, snapshots, scores
class PostNotFoundError(errors.NotFoundError): pass
class PostAlreadyFeaturedError(errors.ValidationError): pass
@@ -36,7 +36,8 @@ def serialize_post(post, authenticated_user):
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
diff --git a/server/szurubooru/func/scores.py b/server/szurubooru/func/scores.py
new file mode 100644
index 0000000..62ee773
--- /dev/null
+++ b/server/szurubooru/func/scores.py
@@ -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)
diff --git a/server/szurubooru/func/snapshots.py b/server/szurubooru/func/snapshots.py
index 7399bdb..6543e0f 100644
--- a/server/szurubooru/func/snapshots.py
+++ b/server/szurubooru/func/snapshots.py
@@ -1,6 +1,6 @@
import datetime
-from sqlalchemy.inspection import inspect
from szurubooru import db
+from szurubooru.func import util
def get_tag_snapshot(tag):
return {
@@ -33,33 +33,11 @@ def get_tag_category_snapshot(category):
# pylint: disable=invalid-name
serializers = {
- 'tag': (
- get_tag_snapshot,
- lambda tag: tag.first_name),
- 'tag_category': (
- get_tag_category_snapshot,
- lambda category: category.name),
- 'post': (
- get_post_snapshot,
- lambda post: post.post_id),
+ 'tag': get_tag_snapshot,
+ 'tag_category': get_tag_category_snapshot,
+ 'post': get_post_snapshot,
}
-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):
return db.session \
.query(db.Snapshot) \
@@ -71,7 +49,7 @@ def get_previous_snapshot(snapshot):
.first()
def get_snapshots(entity):
- resource_type, resource_id, _ = get_resource_info(entity)
+ resource_type, resource_id, _ = util.get_resource_info(entity)
return db.session \
.query(db.Snapshot) \
.filter(db.Snapshot.resource_type == resource_type) \
@@ -103,7 +81,7 @@ def get_serialized_history(entity):
return ret
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()
snapshot = db.Snapshot()
@@ -112,7 +90,7 @@ def save(operation, entity, auth_user):
snapshot.resource_type = resource_type
snapshot.resource_id = resource_id
snapshot.resource_repr = resource_repr
- snapshot.data = serializers[resource_type][0](entity)
+ snapshot.data = serializers[resource_type](entity)
snapshot.user = auth_user
earlier_snapshots = get_snapshots(entity)
diff --git a/server/szurubooru/func/util.py b/server/szurubooru/func/util.py
index 6c14d96..83d3931 100644
--- a/server/szurubooru/func/util.py
+++ b/server/szurubooru/func/util.py
@@ -1,7 +1,31 @@
import datetime
import re
+from sqlalchemy.inspection import inspect
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):
''' Return whether given email address is valid or empty. '''
return not email or re.match(r'^[^@]*@[^@]*\.[^@]*$', email)
diff --git a/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py b/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py
new file mode 100644
index 0000000..b40f663
--- /dev/null
+++ b/server/szurubooru/migrations/versions/ed6dd16a30f3_delete_post_columns.py
@@ -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))
diff --git a/server/szurubooru/tests/api/test_comment_rating.py b/server/szurubooru/tests/api/test_comment_rating.py
new file mode 100644
index 0000000..a5bcce8
--- /dev/null
+++ b/server/szurubooru/tests/api/test_comment_rating.py
@@ -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)
diff --git a/server/szurubooru/tests/api/test_post_rating.py b/server/szurubooru/tests/api/test_post_rating.py
new file mode 100644
index 0000000..26e01db
--- /dev/null
+++ b/server/szurubooru/tests/api/test_post_rating.py
@@ -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)

© 2015 - 2026 Jakob L. Kreuze