From e632324f727b6cbd7609e2d6ab16d6f103bcecf5 Mon Sep 17 00:00:00 2001 From: rr- Date: Mon, 2 May 2016 21:58:13 +0200 Subject: [PATCH] server/posts: add post updating --- API.md | 58 ++++++++- server/szurubooru/api/post_api.py | 36 ++++++ .../tests/api/test_post_creating.py | 6 +- .../tests/api/test_post_updating.py | 115 ++++++++++++++++++ .../tests/api/test_tag_category_creating.py | 2 +- 5 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 server/szurubooru/tests/api/test_post_updating.py diff --git a/API.md b/API.md index 4d52409d..2835d3f4 100644 --- a/API.md +++ b/API.md @@ -30,7 +30,7 @@ - Posts - ~~Listing posts~~ - [Creating post](#creating-post) - - ~~Updating post~~ + - [Updating post](#updating-post) - [Getting post](#getting-post) - [Deleting post](#deleting-post) - [Rating post](#rating-post) @@ -539,7 +539,7 @@ data. - **Errors** - tags have invalid names - - safety is invalid + - safety, notes or flags are invalid - relations refer to non-existing posts - privileges are too low @@ -548,9 +548,57 @@ data. Creates a new post. If specified tags do not exist yet, they will be automatically created. Tags created automatically have no implications, no suggestions, one name and their category is set to the first tag category - found. Safety must be any of `"safe"`, `"sketchy"` or `"unsafe"`. `` - currently can be only `"loop"` to enable looping for video posts. Sending - empty `thumbnail` will cause the post to use default thumbnail. + found. Safety must be any of `"safe"`, `"sketchy"` or `"unsafe"`. Relations + must contain valid post IDs. `` currently can be only `"loop"` to + enable looping for video posts. Sending empty `thumbnail` will cause the + post to use default thumbnail. All fields are optional - update concerns + only provided fields. For details how to pass `content` and `thumbnail`, + see [file uploads](#file-uploads) for details. + +## Updating post +- **Request** + + `PUT /post/` + +- **Input** + + ```json5 + { + "tags": [, , ], // optional + "safety": , // optional + "source": , // optional + "relations": [, , ], // optional + "notes": [, , ], // optional + "flags": [, ] // optional + } + ``` + +- **Files** + + - `content` - the content of the content (optional). + - `thumbnail` - the content of custom thumbnail (optional). + +- **Output** + + A [detailed post resource](#detailed-post). + +- **Errors** + + - tags have invalid names + - safety, notes or flags are invalid + - relations refer to non-existing posts + - privileges are too low + +- **Description** + + Updates existing post. If specified tags do not exist yet, they will be + automatically created. Tags created automatically have no implications, no + suggestions, one name and their category is set to the first tag category + found. Safety must be any of `"safe"`, `"sketchy"` or `"unsafe"`. Relations + must contain valid post IDs. `` currently can be only `"loop"` to + enable looping for video posts. Sending empty `thumbnail` will reset the + post thumbnail to default. For details how to pass `content` and + `thumbnail`, see [file uploads](#file-uploads) for details. ## Getting post - **Request** diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index a299dc42..6200d706 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -1,3 +1,4 @@ +import datetime from szurubooru.api.base_api import BaseApi from szurubooru.func import auth, tags, posts, snapshots, favorites, scores @@ -18,6 +19,8 @@ class PostListApi(BaseApi): posts.update_post_relations(post, relations) posts.update_post_notes(post, notes) posts.update_post_flags(post, flags) + if ctx.has_file('thumbnail'): + posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) ctx.session.add(post) snapshots.save_entity_creation(post, ctx.user) ctx.session.commit() @@ -30,6 +33,39 @@ class PostDetailApi(BaseApi): post = posts.get_post_by_id(post_id) return posts.serialize_post_with_details(post, ctx.user) + def put(self, ctx, post_id): + post = posts.get_post_by_id(post_id) + if ctx.has_file('content'): + auth.verify_privilege(ctx.user, 'posts:edit:content') + posts.update_post_content(post, ctx.get_file('content')) + if ctx.has_param('tags'): + auth.verify_privilege(ctx.user, 'posts:edit:tags') + posts.update_post_tags(post, ctx.get_param_as_list('tags')) + if ctx.has_param('safety'): + auth.verify_privilege(ctx.user, 'posts:edit:safety') + posts.update_post_safety(post, ctx.get_param_as_string('safety')) + if ctx.has_param('source'): + auth.verify_privilege(ctx.user, 'posts:edit:source') + posts.update_post_source(post, ctx.get_param_as_string('source')) + if ctx.has_param('relations'): + auth.verify_privilege(ctx.user, 'posts:edit:relations') + posts.update_post_relations(post, ctx.get_param_as_list('relations')) + if ctx.has_param('notes'): + auth.verify_privilege(ctx.user, 'posts:edit:notes') + posts.update_post_notes(post, ctx.get_param_as_list('notes')) + if ctx.has_param('flags'): + auth.verify_privilege(ctx.user, 'posts:edit:flags') + posts.update_post_flags(post, ctx.get_param_as_list('flags')) + if ctx.has_file('thumbnail'): + auth.verify_privilege(ctx.user, 'posts:edit:thumbnail') + posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) + post.last_edit_time = datetime.datetime.now() + ctx.session.flush() + snapshots.save_entity_modification(post, ctx.user) + ctx.session.commit() + tags.export_to_json() + return posts.serialize_post_with_details(post, ctx.user) + def delete(self, ctx, post_id): auth.verify_privilege(ctx.user, 'posts:delete') post = posts.get_post_by_id(post_id) diff --git a/server/szurubooru/tests/api/test_post_creating.py b/server/szurubooru/tests/api/test_post_creating.py index 9467506e..54e9a491 100644 --- a/server/szurubooru/tests/api/test_post_creating.py +++ b/server/szurubooru/tests/api/test_post_creating.py @@ -25,6 +25,7 @@ def test_creating_minimal_posts( unittest.mock.patch('szurubooru.func.posts.update_post_relations'), \ unittest.mock.patch('szurubooru.func.posts.update_post_notes'), \ unittest.mock.patch('szurubooru.func.posts.update_post_flags'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_thumbnail'), \ unittest.mock.patch('szurubooru.func.posts.serialize_post_with_details'), \ unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'): @@ -40,17 +41,20 @@ def test_creating_minimal_posts( }, files={ 'content': 'post-content', + 'thumbnail': 'post-thumbnail', }, user=auth_user)) assert result == 'serialized post' posts.create_post.assert_called_once_with( 'post-content', ['tag1', 'tag2'], auth_user) + posts.update_post_thumbnail.assert_called_once_with(post, 'post-thumbnail') posts.update_post_safety.assert_called_once_with(post, 'safe') posts.update_post_source.assert_called_once_with(post, None) posts.update_post_relations.assert_called_once_with(post, []) posts.update_post_notes.assert_called_once_with(post, []) posts.update_post_flags.assert_called_once_with(post, []) + posts.update_post_thumbnail.assert_called_once_with(post, 'post-thumbnail') posts.serialize_post_with_details.assert_called_once_with(post, auth_user) tags.export_to_json.assert_called_once_with() snapshots.save_entity_creation.assert_called_once_with(post, auth_user) @@ -129,5 +133,5 @@ def test_trying_to_create_without_privileges(context_factory, user_factory): with pytest.raises(errors.AuthError): api.PostListApi().post( context_factory( - input={'name': 'meta', 'colro': 'black'}, + input='whatever', user=user_factory(rank='anonymous'))) diff --git a/server/szurubooru/tests/api/test_post_updating.py b/server/szurubooru/tests/api/test_post_updating.py new file mode 100644 index 00000000..7498f531 --- /dev/null +++ b/server/szurubooru/tests/api/test_post_updating.py @@ -0,0 +1,115 @@ +import datetime +import os +import unittest.mock +import pytest +from szurubooru import api, db, errors +from szurubooru.func import posts, tags, snapshots + +def test_post_updating( + config_injector, context_factory, post_factory, user_factory, fake_datetime): + config_injector({ + 'ranks': ['anonymous', 'regular_user'], + 'privileges': { + 'posts:edit:tags': 'regular_user', + 'posts:edit:content': 'regular_user', + 'posts:edit:safety': 'regular_user', + 'posts:edit:source': 'regular_user', + 'posts:edit:relations': 'regular_user', + 'posts:edit:notes': 'regular_user', + 'posts:edit:flags': 'regular_user', + 'posts:edit:thumbnail': 'regular_user', + }, + }) + auth_user = user_factory(rank='regular_user') + post = post_factory() + db.session.add(post) + db.session.flush() + + with unittest.mock.patch('szurubooru.func.posts.create_post'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_tags'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_thumbnail'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_safety'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_source'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_relations'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_notes'), \ + unittest.mock.patch('szurubooru.func.posts.update_post_flags'), \ + unittest.mock.patch('szurubooru.func.posts.serialize_post_with_details'), \ + unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ + unittest.mock.patch('szurubooru.func.snapshots.save_entity_modification'): + + posts.serialize_post_with_details.return_value = 'serialized post' + + with fake_datetime('1997-01-01'): + result = api.PostDetailApi().put( + context_factory( + input={ + 'safety': 'safe', + 'tags': ['tag1', 'tag2'], + 'relations': [1, 2], + 'source': 'source', + 'notes': ['note1', 'note2'], + 'flags': ['flag1', 'flag2'], + }, + files={ + 'content': 'post-content', + 'thumbnail': 'post-thumbnail', + }, + user=auth_user), + post.post_id) + + assert result == 'serialized post' + posts.create_post.assert_not_called() + posts.update_post_tags.assert_called_once_with(post, ['tag1', 'tag2']) + posts.update_post_content.assert_called_once_with(post, 'post-content') + posts.update_post_thumbnail.assert_called_once_with(post, 'post-thumbnail') + posts.update_post_safety.assert_called_once_with(post, 'safe') + posts.update_post_source.assert_called_once_with(post, 'source') + posts.update_post_relations.assert_called_once_with(post, [1, 2]) + posts.update_post_notes.assert_called_once_with(post, ['note1', 'note2']) + posts.update_post_flags.assert_called_once_with(post, ['flag1', 'flag2']) + posts.serialize_post_with_details.assert_called_once_with(post, auth_user) + tags.export_to_json.assert_called_once_with() + snapshots.save_entity_modification.assert_called_once_with(post, auth_user) + assert post.last_edit_time == datetime.datetime(1997, 1, 1) + +def test_trying_to_update_non_existing(context_factory, user_factory): + with pytest.raises(posts.PostNotFoundError): + api.PostDetailApi().put( + context_factory( + input='whatever', + user=user_factory(rank='regular_user')), + 1) + +@pytest.mark.parametrize('privilege,files,input', [ + ('posts:edit:tags', {}, {'tags': '...'}), + ('posts:edit:safety', {}, {'safety': '...'}), + ('posts:edit:source', {}, {'source': '...'}), + ('posts:edit:relations', {}, {'relations': '...'}), + ('posts:edit:notes', {}, {'notes': '...'}), + ('posts:edit:flags', {}, {'flags': '...'}), + ('posts:edit:content', {'content': '...'}, {}), + ('posts:edit:thumbnail', {'thumbnail': '...'}, {}), +]) +def test_trying_to_create_without_privileges( + config_injector, + context_factory, + post_factory, + user_factory, + files, + input, + privilege): + config_injector({ + 'ranks': ['anonymous', 'regular_user'], + 'privileges': {privilege: 'regular_user'}, + }) + post = post_factory() + db.session.add(post) + db.session.flush() + with pytest.raises(errors.AuthError): + api.PostDetailApi().put( + context_factory( + input=input, + files=files, + user=user_factory(rank='anonymous')), + post.post_id) diff --git a/server/szurubooru/tests/api/test_tag_category_creating.py b/server/szurubooru/tests/api/test_tag_category_creating.py index dcc0a7a7..ed4b9440 100644 --- a/server/szurubooru/tests/api/test_tag_category_creating.py +++ b/server/szurubooru/tests/api/test_tag_category_creating.py @@ -84,5 +84,5 @@ def test_trying_to_create_without_privileges(test_ctx): with pytest.raises(errors.AuthError): test_ctx.api.post( test_ctx.context_factory( - input={'name': 'meta', 'colro': 'black'}, + input={'name': 'meta', 'color': 'black'}, user=test_ctx.user_factory(rank='anonymous')))