From 48cb172cc83721bc384d756665add1e6c8e8e583 Mon Sep 17 00:00:00 2001
From: rr- <rr-@sakuya.pl>
Date: Sun, 24 Apr 2016 10:13:22 +0200
Subject: [PATCH] server/comments: add comment updating

---
 API.md                                        | 39 +++++++-
 server/szurubooru/api/comment_api.py          | 19 +++-
 server/szurubooru/func/comments.py            |  6 ++
 .../tests/api/test_comment_updating.py        | 97 +++++++++++++++++++
 4 files changed, 157 insertions(+), 4 deletions(-)
 create mode 100644 server/szurubooru/tests/api/test_comment_updating.py

diff --git a/API.md b/API.md
index 504a65f..638ff3e 100644
--- a/API.md
+++ b/API.md
@@ -41,7 +41,7 @@
     - Comments
         - ~~Listing comments~~
         - [Creating comment](#creating-comment)
-        - ~~Updating comment~~
+        - [Updating comment](#updating-comment)
         - ~~Getting comment~~
         - ~~Deleting comment~~
         - ~~Rating comment~~
@@ -698,7 +698,7 @@ data.
 
 - **Errors**
 
-    - post does not exist
+    - the post does not exist
     - comment text is empty
     - privileges are too low
 
@@ -707,6 +707,39 @@ data.
     Creates a new comment under given post.
 
 
+## Updating comment
+- **Request**
+
+    `PUT /comment/<id>`
+
+- **Input**
+
+    ```json5
+    {
+        "text": <new-text>      // mandatory
+    }
+    ```
+
+- **Output**
+
+    ```json5
+    {
+        "comment": <comment>
+    }
+    ```
+    ...where `<comment>` is a [comment resource](#comment).
+
+- **Errors**
+
+    - the comment does not exist
+    - new comment text is empty
+    - privileges are too low
+
+- **Description**
+
+    Updates an existing comment text.
+
+
 ## Listing users
 - **Request**
 
@@ -1225,7 +1258,7 @@ A comment under a post.
 
 ```json5
 {
-    "id":           <comment-id>,
+    "id":           <id>,
     "post":         <post>,
     "user":         <author>
     "text":         <text>,
diff --git a/server/szurubooru/api/comment_api.py b/server/szurubooru/api/comment_api.py
index 2dba81f..23eb0b1 100644
--- a/server/szurubooru/api/comment_api.py
+++ b/server/szurubooru/api/comment_api.py
@@ -1,3 +1,4 @@
+import datetime
 from szurubooru.api.base_api import BaseApi
 from szurubooru.func import auth, comments, posts
 
@@ -23,7 +24,23 @@ class CommentDetailApi(BaseApi):
         raise NotImplementedError()
 
     def put(self, ctx, comment_id):
-        raise NotImplementedError()
+        comment = comments.get_comment_by_id(comment_id)
+        if not comment:
+            raise comments.CommentNotFoundError(
+                'Comment %r not found.' % comment_id)
+
+        if ctx.user.user_id == comment.user_id:
+            infix = 'self'
+        else:
+            infix = 'any'
+
+        comment.last_edit_time = datetime.datetime.now()
+        auth.verify_privilege(ctx.user, 'comments:edit:%s' % infix)
+        text = ctx.get_param_as_string('text', required=True)
+        comments.update_comment_text(comment, text)
+
+        ctx.session.commit()
+        return {'comment': comments.serialize_comment(comment, ctx.user)}
 
     def delete(self, ctx, comment_id):
         raise NotImplementedError()
diff --git a/server/szurubooru/func/comments.py b/server/szurubooru/func/comments.py
index 2ee3439..873bc7d 100644
--- a/server/szurubooru/func/comments.py
+++ b/server/szurubooru/func/comments.py
@@ -15,6 +15,12 @@ def serialize_comment(comment, authenticated_user):
         'lastEditTime': comment.last_edit_time,
     }
 
+def get_comment_by_id(comment_id):
+    return db.session \
+        .query(db.Comment) \
+        .filter(db.Comment.comment_id == comment_id) \
+        .one_or_none()
+
 def create_comment(user, post, text):
     comment = db.Comment()
     comment.user = user
diff --git a/server/szurubooru/tests/api/test_comment_updating.py b/server/szurubooru/tests/api/test_comment_updating.py
new file mode 100644
index 0000000..a0e0a4b
--- /dev/null
+++ b/server/szurubooru/tests/api/test_comment_updating.py
@@ -0,0 +1,97 @@
+import datetime
+import pytest
+from szurubooru import api, db, errors
+from szurubooru.func import util, comments
+
+@pytest.fixture
+def test_ctx(config_injector, context_factory, user_factory, comment_factory):
+    config_injector({
+        'ranks': ['anonymous', 'regular_user', 'mod'],
+        'rank_names': {'anonymous': 'Peasant', 'regular_user': 'Lord', 'mod': 'King'},
+        'privileges': {
+            'comments:edit:self': 'regular_user',
+            'comments:edit:any': 'mod',
+        },
+        '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.CommentDetailApi()
+    return ret
+
+def test_simple_updating(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={'text': 'new text'}, user=user),
+            comment.comment_id)
+    assert result['comment']['text'] == 'new text'
+    comment = db.session.query(db.Comment).one()
+    assert comment is not None
+    assert comment.text == 'new text'
+    assert comment.last_edit_time is not None
+
+@pytest.mark.parametrize('input,expected_exception', [
+    ({'text': None}, comments.EmptyCommentTextError),
+    ({'text': ''}, comments.EmptyCommentTextError),
+    ({'text': []}, comments.EmptyCommentTextError),
+    ({'text': [None]}, errors.ValidationError),
+    ({'text': ['']}, comments.EmptyCommentTextError),
+])
+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={'text': 'new text'},
+                user=test_ctx.user_factory(rank='regular_user')),
+            5)
+
+def test_trying_to_update_someones_comment_without_privileges(test_ctx):
+    user = test_ctx.user_factory(rank='regular_user')
+    user2 = test_ctx.user_factory(rank='regular_user')
+    comment = test_ctx.comment_factory(user=user)
+    db.session.add(comment)
+    db.session.commit()
+    with pytest.raises(errors.AuthError):
+        test_ctx.api.put(
+            test_ctx.context_factory(input={'text': 'new text'}, user=user2),
+            comment.comment_id)
+
+def test_updating_someones_comment_with_privileges(test_ctx):
+    user = test_ctx.user_factory(rank='regular_user')
+    user2 = test_ctx.user_factory(rank='mod')
+    comment = test_ctx.comment_factory(user=user)
+    db.session.add(comment)
+    db.session.commit()
+    try:
+        test_ctx.api.put(
+            test_ctx.context_factory(input={'text': 'new text'}, user=user2),
+            comment.comment_id)
+    except:
+        pytest.fail()