server/posts: allow anonymous uploads (#90)
This commit is contained in:
parent
a20bf56e75
commit
508cb6e7ab
12
API.md
12
API.md
|
@ -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**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue