server/posts: allow anonymous uploads (#90)

This commit is contained in:
rr- 2016-06-05 10:30:10 +02:00
parent a20bf56e75
commit 508cb6e7ab
5 changed files with 172 additions and 87 deletions

12
API.md
View File

@ -676,7 +676,8 @@ data.
"source": <source>, // optional
"relations": [<post1>, <post2>, <post3>], // optional
"notes": [<note1>, <note2>, <note3>], // optional
"flags": [<flag1>, <flag2>] // optional
"flags": [<flag1>, <flag2>], // optional
"anonymous": <anonymous> // optional
}
```
@ -704,9 +705,12 @@ data.
found. Safety must be any of `"safe"`, `"sketchy"` or `"unsafe"`. Relations
must contain valid post IDs. `<flag>` 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.
post to use default thumbnail. If `anonymous` is set to truthy value, the
uploader name won't be recorded (privilege verification still applies; it's
possible to disallow anonymous uploads completely from config.) 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**

View File

@ -2,6 +2,26 @@ import falcon
from szurubooru import errors
from szurubooru.func import net
def _lower_first(input):
return input[0].lower() + input[1:]
def _param_wrapper(func):
def wrapper(self, name, required=False, default=None, **kwargs):
if name in self.input:
value = self.input[name]
try:
value = func(self, value, **kwargs)
except errors.InvalidParameterError as e:
raise errors.InvalidParameterError(
'Parameter %r is invalid: %s' % (
name, _lower_first(str(e))))
return value
if not required:
return default
raise errors.MissingRequiredParameterError(
'Required parameter %r is missing.' % name)
return wrapper
class Context(object):
def __init__(self):
self.session = None
@ -27,57 +47,45 @@ class Context(object):
raise errors.MissingRequiredFileError(
'Required file %r is missing.' % name)
def get_param_as_list(self, name, required=False, default=None):
if name in self.input:
param = self.input[name]
if not isinstance(param, list):
return [param]
return param
if not required:
return default
raise errors.MissingRequiredParameterError(
'Required paramter %r is missing.' % name)
@_param_wrapper
def get_param_as_list(self, value):
if not isinstance(value, list):
return [value]
return value
def get_param_as_string(self, name, required=False, default=None):
if name in self.input:
param = self.input[name]
if isinstance(param, list):
try:
param = ','.join(param)
except:
raise errors.InvalidParameterError(
'Parameter %r is invalid - expected simple string.'
% name)
return param
if not required:
return default
raise errors.MissingRequiredParameterError(
'Required paramter %r is missing.' % name)
# pylint: disable=redefined-builtin,too-many-arguments
def get_param_as_int(
self, name, required=False, min=None, max=None, default=None):
if name in self.input:
val = self.input[name]
@_param_wrapper
def get_param_as_string(self, value):
if isinstance(value, list):
try:
val = int(val)
except (ValueError, TypeError):
raise errors.InvalidParameterError(
'Parameter %r is invalid: the value must be an integer.'
% name)
if min is not None and val < min:
raise errors.InvalidParameterError(
'Parameter %r is invalid: the value must be at least %r.'
% (name, min))
if max is not None and val > max:
raise errors.InvalidParameterError(
'Parameter %r is invalid: the value may not exceed %r.'
% (name, max))
return val
if not required:
return default
raise errors.MissingRequiredParameterError(
'Required parameter %r is missing.' % name)
value = ','.join(value)
except:
raise errors.InvalidParameterError('Expected simple string.')
return value
# pylint: disable=redefined-builtin
@_param_wrapper
def get_param_as_int(self, value, min=None, max=None):
try:
value = int(value)
except (ValueError, TypeError):
raise errors.InvalidParameterError(
'The value must be an integer.')
if min is not None and value < min:
raise errors.InvalidParameterError(
'The value must be at least %r.' % min)
if max is not None and value > max:
raise errors.InvalidParameterError(
'The value may not exceed %r.' % max)
return value
@_param_wrapper
def get_param_as_bool(self, value):
value = str(value).lower()
if value in ['1', 'y', 'yes', 'yeah', 'yep', 'yup', 't', 'true']:
return True
if value in ['0', 'n', 'no', 'nope', 'f', 'false']:
return False
raise errors.InvalidParameterError('The value must be a boolean value.')
class Request(falcon.Request):
context_type = Context

View File

@ -22,7 +22,11 @@ class PostListApi(BaseApi):
ctx, lambda post: _serialize_post(ctx, post))
def post(self, ctx):
auth.verify_privilege(ctx.user, 'posts:create')
anonymous = ctx.get_param_as_bool('anonymous', default=False)
if anonymous:
auth.verify_privilege(ctx.user, 'posts:create:anonymous')
else:
auth.verify_privilege(ctx.user, 'posts:create:identified')
content = ctx.get_file('content', required=True)
tag_names = ctx.get_param_as_list('tags', required=True)
safety = ctx.get_param_as_string('safety', required=True)
@ -33,7 +37,8 @@ class PostListApi(BaseApi):
notes = ctx.get_param_as_list('notes', required=False) or []
flags = ctx.get_param_as_list('flags', required=False) or []
post = posts.create_post(content, tag_names, ctx.user)
post = posts.create_post(
content, tag_names, None if anonymous else ctx.user)
posts.update_post_safety(post, safety)
posts.update_post_source(post, source)
posts.update_post_relations(post, relations)

View File

@ -62,3 +62,40 @@ def test_getting_int_parameter():
with pytest.raises(errors.ValidationError):
assert ctx.get_param_as_int('key', max=50) == 50
ctx.get_param_as_int('key', max=49)
def test_getting_bool_parameter():
def test(value):
ctx = api.Context()
ctx.input = {'key': value}
return ctx.get_param_as_bool('key')
assert test('1') is True
assert test('y') is True
assert test('yes') is True
assert test('yep') is True
assert test('yup') is True
assert test('yeah') is True
assert test('t') is True
assert test('true') is True
assert test('TRUE') is True
assert test('0') is False
assert test('n') is False
assert test('no') is False
assert test('nope') is False
assert test('f') is False
assert test('false') is False
assert test('FALSE') is False
with pytest.raises(errors.ValidationError):
test('herp')
with pytest.raises(errors.ValidationError):
test('2')
with pytest.raises(errors.ValidationError):
test(['1', '2'])
ctx = api.Context()
assert ctx.get_param_as_bool('non-existing') is None
assert ctx.get_param_as_bool('non-existing', default=True) is True
with pytest.raises(errors.ValidationError):
assert ctx.get_param_as_bool('non-existing', required=True) is None

View File

@ -8,7 +8,10 @@ from szurubooru.func import posts, tags, snapshots, net
@pytest.fixture(autouse=True)
def inject_config(config_injector):
config_injector({
'privileges': {'posts:create': db.User.RANK_REGULAR},
'privileges': {
'posts:create:anonymous': db.User.RANK_REGULAR,
'posts:create:identified': db.User.RANK_REGULAR,
},
})
def test_creating_minimal_posts(
@ -19,16 +22,15 @@ def test_creating_minimal_posts(
db.session.flush()
with unittest.mock.patch('szurubooru.func.posts.create_post'), \
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.update_post_thumbnail'), \
unittest.mock.patch('szurubooru.func.posts.serialize_post'), \
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'):
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.update_post_thumbnail'), \
unittest.mock.patch('szurubooru.func.posts.serialize_post'), \
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'):
posts.create_post.return_value = post
posts.serialize_post.return_value = 'serialized post'
@ -65,15 +67,14 @@ def test_creating_full_posts(context_factory, post_factory, user_factory):
db.session.flush()
with unittest.mock.patch('szurubooru.func.posts.create_post'), \
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'), \
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'):
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'), \
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'):
posts.create_post.return_value = post
posts.serialize_post.return_value = 'serialized post'
@ -104,6 +105,36 @@ def test_creating_full_posts(context_factory, post_factory, user_factory):
tags.export_to_json.assert_called_once_with()
snapshots.save_entity_creation.assert_called_once_with(post, auth_user)
def test_anonymous_uploads(
config_injector, context_factory, post_factory, user_factory):
auth_user = user_factory(rank=db.User.RANK_REGULAR)
post = post_factory()
db.session.add(post)
db.session.flush()
with unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'), \
unittest.mock.patch('szurubooru.func.posts.serialize_post'), \
unittest.mock.patch('szurubooru.func.posts.create_post'), \
unittest.mock.patch('szurubooru.func.posts.update_post_source'):
config_injector({
'privileges': {'posts:create:anonymous': db.User.RANK_REGULAR},
})
posts.create_post.return_value = post
api.PostListApi().post(
context_factory(
input={
'safety': 'safe',
'tags': ['tag1', 'tag2'],
'anonymous': 'True',
},
files={
'content': 'post-content',
},
user=auth_user))
posts.create_post.assert_called_once_with(
'post-content', ['tag1', 'tag2'], None)
def test_creating_from_url_saves_source(
config_injector, context_factory, post_factory, user_factory):
auth_user = user_factory(rank=db.User.RANK_REGULAR)
@ -112,13 +143,13 @@ def test_creating_from_url_saves_source(
db.session.flush()
with unittest.mock.patch('szurubooru.func.net.download'), \
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'), \
unittest.mock.patch('szurubooru.func.posts.serialize_post'), \
unittest.mock.patch('szurubooru.func.posts.create_post'), \
unittest.mock.patch('szurubooru.func.posts.update_post_source'):
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'), \
unittest.mock.patch('szurubooru.func.posts.serialize_post'), \
unittest.mock.patch('szurubooru.func.posts.create_post'), \
unittest.mock.patch('szurubooru.func.posts.update_post_source'):
config_injector({
'privileges': {'posts:create': db.User.RANK_REGULAR},
'privileges': {'posts:create:identified': db.User.RANK_REGULAR},
})
net.download.return_value = b'content'
posts.create_post.return_value = post
@ -143,13 +174,13 @@ def test_creating_from_url_with_source_specified(
db.session.flush()
with unittest.mock.patch('szurubooru.func.net.download'), \
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'), \
unittest.mock.patch('szurubooru.func.posts.serialize_post'), \
unittest.mock.patch('szurubooru.func.posts.create_post'), \
unittest.mock.patch('szurubooru.func.posts.update_post_source'):
unittest.mock.patch('szurubooru.func.tags.export_to_json'), \
unittest.mock.patch('szurubooru.func.snapshots.save_entity_creation'), \
unittest.mock.patch('szurubooru.func.posts.serialize_post'), \
unittest.mock.patch('szurubooru.func.posts.create_post'), \
unittest.mock.patch('szurubooru.func.posts.update_post_source'):
config_injector({
'privileges': {'posts:create': db.User.RANK_REGULAR},
'privileges': {'posts:create:identified': db.User.RANK_REGULAR},
})
net.download.return_value = b'content'
posts.create_post.return_value = post