server/tags: make creating tag relations optional

This commit is contained in:
rr- 2016-04-19 00:18:35 +02:00
parent 1597ae7c5c
commit 7263849fac
5 changed files with 111 additions and 36 deletions

58
API.md
View file

@ -13,16 +13,25 @@
2. [API reference](#api-reference) 2. [API reference](#api-reference)
- Tag categories
- [Listing tag categories](#listing-tags-category)
- [Creating tag category](#creating-tag-category)
- [Updating tag category](#updating-tag-category)
- [Getting tag category](#getting-tag-category)
- [Deleting tag category](#deleting-tag-category)
- Tags
- [Listing tags](#listing-tags) - [Listing tags](#listing-tags)
- [Creating tag](#creating-tag) - [Creating tag](#creating-tag)
- [Updating tag](#updating-tag) - [Updating tag](#updating-tag)
- [Getting tag](#getting-tag) - [Getting tag](#getting-tag)
- [Deleting tag](#deleting-tag) - [Deleting tag](#deleting-tag)
- Users
- [Listing users](#listing-users) - [Listing users](#listing-users)
- [Creating user](#creating-user) - [Creating user](#creating-user)
- [Updating user](#updating-user) - [Updating user](#updating-user)
- [Getting user](#getting-user) - [Getting user](#getting-user)
- [Deleting user](#deleting-user) - [Deleting user](#deleting-user)
- Password reset
- [Password reset - step 1: mail request](#password-reset---step-2-confirmation) - [Password reset - step 1: mail request](#password-reset---step-2-confirmation)
- [Password reset - step 2: confirmation](#password-reset---step-2-confirmation) - [Password reset - step 2: confirmation](#password-reset---step-2-confirmation)
@ -82,6 +91,31 @@ as `/api/`. Values denoted with diamond braces (`<like this>`) signify variable
data. data.
## Listing tag categories
Not implemented yet.
## Creating tag category
Not implemented yet.
## Updating tag category
Not implemented yet.
## Getting tag category
Not implemented yet.
## Deleting tag category
Not implemented yet.
## Listing tags ## Listing tags
- **Request** - **Request**
@ -178,8 +212,8 @@ data.
{ {
"names": [<name1>, <name2>, ...], "names": [<name1>, <name2>, ...],
"category": <category>, "category": <category>,
"implications": [<name1>, <name2>, ...], "implications": [<name1>, <name2>, ...], // optional
"suggestions": [<name1>, <name2>, ...] "suggestions": [<name1>, <name2>, ...] // optional
} }
``` ```
@ -206,11 +240,12 @@ data.
Creates a new tag using specified parameters. Names, suggestions and Creates a new tag using specified parameters. Names, suggestions and
implications must match `tag_name_regex` from server's configuration. implications must match `tag_name_regex` from server's configuration.
Category must be one of `tag_categories` from server's configuration. Category must exist and is the same as `name` field within
If specified implied tags or suggested tags do not exist yet, they will [`<tag-category>` resource](#tag-category). Suggestions and implications
be automatically created. Tags created automatically have no implications, are optional. If specified implied tags or suggested tags do not exist yet,
no suggestions, one name and their category is set to the first item of they will be automatically created. Tags created automatically have no
`tag_categories` from server's configuration. implications, no suggestions, one name and their category is set to the
first item of `tag_categories` from server's configuration.
## Updating tag ## Updating tag
@ -597,6 +632,15 @@ data.
} }
``` ```
## Tag category
```json5
{
"name": "character",
"color": "#FF0000", // used to colorize certain tag types in the web client
}
```
## Tag ## Tag
```json5 ```json5

View file

@ -40,8 +40,10 @@ class TagListApi(BaseApi):
names = ctx.get_param_as_list('names', required=True) names = ctx.get_param_as_list('names', required=True)
category = ctx.get_param_as_string('category', required=True) category = ctx.get_param_as_string('category', required=True)
suggestions = ctx.get_param_as_list('suggestions', required=True) suggestions = ctx.get_param_as_list(
implications = ctx.get_param_as_list('implications', required=True) 'suggestions', required=False, default=[])
implications = ctx.get_param_as_list(
'implications', required=False, default=[])
tag = tags.create_tag(names, category, suggestions, implications) tag = tags.create_tag(names, category, suggestions, implications)
ctx.session.add(tag) ctx.session.add(tag)

View file

@ -70,6 +70,35 @@ def test_creating_simple_tags(test_ctx, fake_datetime):
assert_relations(tag.implications, []) assert_relations(tag.implications, [])
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
@pytest.mark.parametrize('field', ['names', 'category'])
def test_missing_mandatory_field(test_ctx, field):
input = {
'names': ['tag1', 'tag2'],
'category': 'meta',
'suggestions': [],
'implications': [],
}
del input[field]
with pytest.raises(errors.ValidationError):
test_ctx.api.post(
test_ctx.context_factory(
input=input,
user=test_ctx.user_factory(rank='regular_user')))
@pytest.mark.parametrize('field', ['implications', 'suggestions'])
def test_missing_optional_field(test_ctx, tmpdir, field):
input = {
'names': ['tag1', 'tag2'],
'category': 'meta',
'suggestions': [],
'implications': [],
}
del input[field]
test_ctx.api.post(
test_ctx.context_factory(
input=input,
user=test_ctx.user_factory(rank='regular_user')))
def test_duplicating_names(test_ctx): def test_duplicating_names(test_ctx):
result = test_ctx.api.post( result = test_ctx.api.post(
test_ctx.context_factory( test_ctx.context_factory(
@ -86,7 +115,7 @@ def test_duplicating_names(test_ctx):
assert [tag_name.name for tag_name in tag.names] == ['tag1'] assert [tag_name.name for tag_name in tag.names] == ['tag1']
def test_trying_to_create_tag_without_names(test_ctx): def test_trying_to_create_tag_without_names(test_ctx):
with pytest.raises(tags.InvalidNameError): with pytest.raises(tags.InvalidTagNameError):
test_ctx.api.post( test_ctx.api.post(
test_ctx.context_factory( test_ctx.context_factory(
input={ input={
@ -99,7 +128,7 @@ def test_trying_to_create_tag_without_names(test_ctx):
@pytest.mark.parametrize('names', [['!'], ['x' * 65]]) @pytest.mark.parametrize('names', [['!'], ['x' * 65]])
def test_trying_to_create_tag_with_invalid_name(test_ctx, names): def test_trying_to_create_tag_with_invalid_name(test_ctx, names):
with pytest.raises(tags.InvalidNameError): with pytest.raises(tags.InvalidTagNameError):
test_ctx.api.post( test_ctx.api.post(
test_ctx.context_factory( test_ctx.context_factory(
input={ input={
@ -141,7 +170,7 @@ def test_trying_to_use_existing_name(test_ctx):
assert get_tag(test_ctx.session, 'unused') is None assert get_tag(test_ctx.session, 'unused') is None
def test_trying_to_create_tag_with_invalid_category(test_ctx): def test_trying_to_create_tag_with_invalid_category(test_ctx):
with pytest.raises(tags.InvalidCategoryError): with pytest.raises(tags.InvalidTagCategoryError):
test_ctx.api.post( test_ctx.api.post(
test_ctx.context_factory( test_ctx.context_factory(
input={ input={
@ -233,7 +262,7 @@ def test_reusing_suggestions_and_implications(test_ctx):
} }
]) ])
def test_trying_to_create_tag_with_invalid_relation(test_ctx, input): def test_trying_to_create_tag_with_invalid_relation(test_ctx, input):
with pytest.raises(tags.InvalidNameError): with pytest.raises(tags.InvalidTagNameError):
test_ctx.api.post( test_ctx.api.post(
test_ctx.context_factory( test_ctx.context_factory(
input=input, user=test_ctx.user_factory(rank='regular_user'))) input=input, user=test_ctx.user_factory(rank='regular_user')))

View file

@ -127,7 +127,7 @@ def test_trying_to_set_invalid_name(test_ctx, input):
test_ctx.session.add( test_ctx.session.add(
test_ctx.tag_factory(names=['tag1'], category_name='meta')) test_ctx.tag_factory(names=['tag1'], category_name='meta'))
test_ctx.session.commit() test_ctx.session.commit()
with pytest.raises(tags.InvalidNameError): with pytest.raises(tags.InvalidTagNameError):
test_ctx.api.put( test_ctx.api.put(
test_ctx.context_factory( test_ctx.context_factory(
input=input, input=input,
@ -151,7 +151,7 @@ def test_trying_to_update_tag_with_invalid_category(test_ctx):
test_ctx.session.add( test_ctx.session.add(
test_ctx.tag_factory(names=['tag1'], category_name='meta')) test_ctx.tag_factory(names=['tag1'], category_name='meta'))
test_ctx.session.commit() test_ctx.session.commit()
with pytest.raises(tags.InvalidCategoryError): with pytest.raises(tags.InvalidTagCategoryError):
test_ctx.api.put( test_ctx.api.put(
test_ctx.context_factory( test_ctx.context_factory(
input={ input={
@ -234,7 +234,7 @@ def test_trying_to_update_tag_with_invalid_relation(test_ctx, input):
test_ctx.session.add( test_ctx.session.add(
test_ctx.tag_factory(names=['tag'], category_name='meta')) test_ctx.tag_factory(names=['tag'], category_name='meta'))
test_ctx.session.commit() test_ctx.session.commit()
with pytest.raises(tags.InvalidNameError): with pytest.raises(tags.InvalidTagNameError):
test_ctx.api.put( test_ctx.api.put(
test_ctx.context_factory( test_ctx.context_factory(
input=input, user=test_ctx.user_factory(rank='regular_user')), input=input, user=test_ctx.user_factory(rank='regular_user')),

View file

@ -8,15 +8,15 @@ from szurubooru.util import misc
class TagNotFoundError(errors.NotFoundError): pass class TagNotFoundError(errors.NotFoundError): pass
class TagAlreadyExistsError(errors.ValidationError): pass class TagAlreadyExistsError(errors.ValidationError): pass
class InvalidNameError(errors.ValidationError): pass
class InvalidCategoryError(errors.ValidationError): pass
class RelationError(errors.ValidationError): pass
class TagIsInUseError(errors.ValidationError): pass class TagIsInUseError(errors.ValidationError): pass
class InvalidTagNameError(errors.ValidationError): pass
class InvalidTagCategoryError(errors.ValidationError): pass
class RelationError(errors.ValidationError): pass
def _verify_name_validity(name): def _verify_name_validity(name):
name_regex = config.config['tag_name_regex'] name_regex = config.config['tag_name_regex']
if not re.match(name_regex, name): if not re.match(name_regex, name):
raise InvalidNameError('Name must satisfy regex %r.' % name_regex) raise InvalidTagNameError('Name must satisfy regex %r.' % name_regex)
def _get_plain_names(tag): def _get_plain_names(tag):
return [tag_name.name for tag_name in tag.names] return [tag_name.name for tag_name in tag.names]
@ -105,7 +105,7 @@ def update_category_name(tag, category_name):
if not category: if not category:
category_names = [ category_names = [
name[0] for name in session.query(db.TagCategory.name).all()] name[0] for name in session.query(db.TagCategory.name).all()]
raise InvalidCategoryError( raise InvalidTagCategoryError(
'Category %r is invalid. Valid categories: %r.' % ( 'Category %r is invalid. Valid categories: %r.' % (
category_name, category_names)) category_name, category_names))
tag.category = category tag.category = category
@ -113,13 +113,13 @@ def update_category_name(tag, category_name):
def update_names(tag, names): def update_names(tag, names):
names = misc.icase_unique(names) names = misc.icase_unique(names)
if not len(names): if not len(names):
raise InvalidNameError('At least one name must be specified.') raise InvalidTagNameError('At least one name must be specified.')
for name in names: for name in names:
_verify_name_validity(name) _verify_name_validity(name)
expr = sqlalchemy.sql.false() expr = sqlalchemy.sql.false()
for name in names: for name in names:
if misc.value_exceeds_column_size(name, db.TagName.name): if misc.value_exceeds_column_size(name, db.TagName.name):
raise InvalidNameError('Name is too long.') raise InvalidTagNameError('Name is too long.')
expr = expr | db.TagName.name.ilike(name) expr = expr | db.TagName.name.ilike(name)
if tag.tag_id: if tag.tag_id:
expr = expr & (db.TagName.tag_id != tag.tag_id) expr = expr & (db.TagName.tag_id != tag.tag_id)