diff --git a/API.md b/API.md index 4d52409..2835d3f 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 a299dc4..6200d70 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 9467506..54e9a49 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 0000000..7498f53 --- /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 dcc0a7a..ed4b944 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')))