server/tags: add tag updating
This commit is contained in:
parent
018dedcffd
commit
adecdd4cd9
55
API.md
55
API.md
|
@ -85,6 +85,7 @@ data.
|
|||
## Listing tags
|
||||
Not yet implemented.
|
||||
|
||||
|
||||
## Creating tag
|
||||
- **Request**
|
||||
|
||||
|
@ -116,6 +117,8 @@ Not yet implemented.
|
|||
- any name, implication or suggestion has invalid name
|
||||
- category is invalid
|
||||
- no name was specified
|
||||
- implications or suggestions contain any item from names (e.g. there's a
|
||||
shallow cyclic dependency)
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
@ -128,12 +131,58 @@ Not yet implemented.
|
|||
no suggestions, one name and their category is set to the first item of
|
||||
`tag_categories` from server's configuration.
|
||||
|
||||
|
||||
## Updating tag
|
||||
Not yet implemented.
|
||||
- **Request**
|
||||
|
||||
`PUT /tags/<name>`
|
||||
|
||||
- **Input**
|
||||
|
||||
```json5
|
||||
{
|
||||
"names": [<name1>, <name2>, ...],
|
||||
"category": <category>,
|
||||
"implications": [<name1>, <name2>, ...],
|
||||
"suggestions": [<name1>, <name2>, ...]
|
||||
}
|
||||
```
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"tag": <tag>
|
||||
}
|
||||
```
|
||||
...where `<tag>` is a [tag resource](#tag).
|
||||
|
||||
- **Errors**
|
||||
|
||||
- any name is used by an existing tag (names are case insensitive)
|
||||
- any name, implication or suggestion has invalid name
|
||||
- category is invalid
|
||||
- no name was specified
|
||||
- implications or suggestions contain any item from names (e.g. there's a
|
||||
shallow cyclic dependency)
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Updates an existing tag using specified parameters. Names, suggestions and
|
||||
implications must match `tag_name_regex` from server's configuration.
|
||||
Category must be one of `tag_categories` from server's configuration.
|
||||
If specified implied tags or suggested 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 item of
|
||||
`tag_categories` from server's configuration. All fields are optional -
|
||||
update concerns only provided fields.
|
||||
|
||||
|
||||
## Getting tag
|
||||
Not yet implemented.
|
||||
|
||||
|
||||
## Removing tag
|
||||
Not yet implemented.
|
||||
|
||||
|
@ -429,7 +478,9 @@ Not yet implemented.
|
|||
"names": ["tag1", "tag2", "tag3"],
|
||||
"category": "plain", // one of values controlled by server's configuration
|
||||
"implications": ["implied-tag1", "implied-tag2", "implied-tag3"],
|
||||
"suggestions": ["suggested-tag1", "suggested-tag2", "suggested-tag3"]
|
||||
"suggestions": ["suggested-tag1", "suggested-tag2", "suggested-tag3"],
|
||||
"creationTime": "2016-03-28T13:37:01.755461",
|
||||
"lastEditTime": "2016-04-08T20:20:16.570517"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import datetime
|
||||
from szurubooru import errors
|
||||
from szurubooru.util import auth, tags
|
||||
from szurubooru.api.base_api import BaseApi
|
||||
|
@ -10,6 +11,8 @@ def _serialize_tag(tag):
|
|||
relation.child_tag.names[0].name for relation in tag.suggestions],
|
||||
'implications': [
|
||||
relation.child_tag.names[0].name for relation in tag.implications],
|
||||
'creationTime': tag.creation_time,
|
||||
'lastEditTime': tag.last_edit_time,
|
||||
}
|
||||
|
||||
class TagListApi(BaseApi):
|
||||
|
@ -27,7 +30,6 @@ class TagListApi(BaseApi):
|
|||
tag = tags.create_tag(
|
||||
ctx.session, names, category, suggestions, implications)
|
||||
ctx.session.add(tag)
|
||||
ctx.session.flush()
|
||||
ctx.session.commit()
|
||||
return {'tag': _serialize_tag(tag)}
|
||||
|
||||
|
@ -35,8 +37,34 @@ class TagDetailApi(BaseApi):
|
|||
def get(self, ctx):
|
||||
raise NotImplementedError()
|
||||
|
||||
def put(self, ctx):
|
||||
raise NotImplementedError()
|
||||
def put(self, ctx, tag_name):
|
||||
tag = tags.get_by_name(ctx.session, tag_name)
|
||||
if not tag:
|
||||
raise tags.TagNotFoundError('Tag %r not found.' % tag_name)
|
||||
|
||||
if ctx.has_param('names'):
|
||||
auth.verify_privilege(ctx.user, 'tags:edit:names')
|
||||
tags.update_names(
|
||||
ctx.session, tag, ctx.get_param_as_list('names'))
|
||||
|
||||
if ctx.has_param('category'):
|
||||
auth.verify_privilege(ctx.user, 'tags:edit:category')
|
||||
tags.update_category(
|
||||
ctx.session, tag, ctx.get_param_as_string('category'))
|
||||
|
||||
if ctx.has_param('suggestions'):
|
||||
auth.verify_privilege(ctx.user, 'tags:edit:suggestions')
|
||||
tags.update_suggestions(
|
||||
ctx.session, tag, ctx.get_param_as_list('suggestions'))
|
||||
|
||||
if ctx.has_param('implications'):
|
||||
auth.verify_privilege(ctx.user, 'tags:edit:implications')
|
||||
tags.update_implications(
|
||||
ctx.session, tag, ctx.get_param_as_list('implications'))
|
||||
|
||||
tag.last_edit_time = datetime.datetime.now()
|
||||
ctx.session.commit()
|
||||
return {'tag': _serialize_tag(tag)}
|
||||
|
||||
def delete(self, ctx):
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -1,241 +1,256 @@
|
|||
import datetime
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from szurubooru import api, db, errors
|
||||
from szurubooru.util import auth
|
||||
from szurubooru.util import misc, tags
|
||||
|
||||
def get_tag(session, name):
|
||||
return session.query(db.Tag) \
|
||||
.join(db.TagName) \
|
||||
.filter(db.TagName.name==name) \
|
||||
.first()
|
||||
|
||||
def assert_relations(relations, expected_tag_names):
|
||||
actual_names = [rel.child_tag.names[0].name for rel in relations]
|
||||
assert actual_names == expected_tag_names
|
||||
|
||||
@pytest.fixture
|
||||
def tag_config(config_injector):
|
||||
def test_ctx(
|
||||
session, config_injector, context_factory, user_factory, tag_factory):
|
||||
config_injector({
|
||||
'tag_categories': ['meta', 'character', 'copyright'],
|
||||
'tag_name_regex': '^[^!]*$',
|
||||
'ranks': ['regular_user'],
|
||||
'privileges': {'tags:create': 'regular_user'},
|
||||
})
|
||||
ret = misc.dotdict()
|
||||
ret.session = session
|
||||
ret.context_factory = context_factory
|
||||
ret.user_factory = user_factory
|
||||
ret.tag_factory = tag_factory
|
||||
ret.api = api.TagListApi()
|
||||
return ret
|
||||
|
||||
@pytest.fixture
|
||||
def tag_list_api(tag_config):
|
||||
return api.TagListApi()
|
||||
|
||||
def get_tag(session, name):
|
||||
return session.query(db.Tag) \
|
||||
.join(db.TagName) \
|
||||
.filter(db.TagName.name==name) \
|
||||
.one()
|
||||
|
||||
def assert_relations(relations, expected_tag_names):
|
||||
actual_names = [rel.child_tag.names[0].name for rel in relations]
|
||||
assert actual_names == expected_tag_names
|
||||
|
||||
def test_creating_simple_tags(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
result = tag_list_api.post(
|
||||
context_factory(
|
||||
def test_creating_simple_tags(test_ctx, fake_datetime):
|
||||
fake_datetime(datetime.datetime(1997, 12, 1))
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['tag1', 'tag2'],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert result == {
|
||||
'tag': {
|
||||
'names': ['tag1', 'tag2'],
|
||||
'category': 'meta',
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
'creationTime': datetime.datetime(1997, 12, 1),
|
||||
'lastEditTime': None,
|
||||
}
|
||||
}
|
||||
tag = get_tag(session, 'tag1')
|
||||
tag = get_tag(test_ctx.session, 'tag1')
|
||||
assert [tag_name.name for tag_name in tag.names] == ['tag1', 'tag2']
|
||||
assert tag.category == 'meta'
|
||||
#TODO: assert tag.creation_time == something
|
||||
assert tag.last_edit_time is None
|
||||
assert tag.post_count == 0
|
||||
assert_relations(tag.suggestions, [])
|
||||
assert_relations(tag.implications, [])
|
||||
|
||||
def test_duplicating_names(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
result = tag_list_api.post(
|
||||
context_factory(
|
||||
def test_duplicating_names(test_ctx):
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['tag1', 'TAG1'],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert result['tag']['names'] == ['tag1']
|
||||
assert result['tag']['category'] == 'meta'
|
||||
tag = get_tag(session, 'tag1')
|
||||
tag = get_tag(test_ctx.session, 'tag1')
|
||||
assert [tag_name.name for tag_name in tag.names] == ['tag1']
|
||||
|
||||
def test_trying_to_create_tag_without_names(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
def test_trying_to_create_tag_without_names(test_ctx):
|
||||
with pytest.raises(tags.InvalidNameError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': [],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
|
||||
def test_trying_to_use_existing_name(
|
||||
session, context_factory, user_factory, tag_factory, tag_list_api):
|
||||
session.add(tag_factory(names=['used1'], category='meta'))
|
||||
session.add(tag_factory(names=['used2'], category='meta'))
|
||||
session.commit()
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['used1', 'unused'],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['USED2', 'unused'],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
|
||||
def test_trying_to_create_tag_with_invalid_name(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
def test_trying_to_create_tag_with_invalid_name(test_ctx):
|
||||
with pytest.raises(tags.InvalidNameError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['!'],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['ok'],
|
||||
'category': 'meta',
|
||||
'implications': ['!'],
|
||||
'suggestions': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['ok'],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': ['!'],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert get_tag(test_ctx.session, 'ok') is None
|
||||
assert get_tag(test_ctx.session, '!') is None
|
||||
|
||||
def test_trying_to_create_tag_with_invalid_category(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
def test_trying_to_use_existing_name(test_ctx):
|
||||
test_ctx.session.add_all([
|
||||
test_ctx.tag_factory(names=['used1'], category='meta'),
|
||||
test_ctx.tag_factory(names=['used2'], category='meta'),
|
||||
])
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(tags.TagAlreadyExistsError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['used1', 'unused'],
|
||||
'category': 'meta',
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
},
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
with pytest.raises(tags.TagAlreadyExistsError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['USED2', 'unused'],
|
||||
'category': 'meta',
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
},
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert get_tag(test_ctx.session, 'unused') is None
|
||||
|
||||
def test_trying_to_create_tag_with_invalid_category(test_ctx):
|
||||
with pytest.raises(tags.InvalidCategoryError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['ok'],
|
||||
'category': 'invalid',
|
||||
'implications': [],
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert get_tag(test_ctx.session, 'ok') is None
|
||||
|
||||
@pytest.mark.parametrize('input,expected_suggestions,expected_implications', [
|
||||
# new relations
|
||||
({
|
||||
'names': ['main'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['sug1', 'sug2'],
|
||||
'implications': ['imp1', 'imp2'],
|
||||
}, ['sug1', 'sug2'], ['imp1', 'imp2']),
|
||||
# overlapping relations
|
||||
({
|
||||
'names': ['main'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['sug', 'shared'],
|
||||
'implications': ['shared', 'imp'],
|
||||
}, ['sug', 'shared'], ['shared', 'imp']),
|
||||
# duplicate relations
|
||||
({
|
||||
'names': ['main'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['sug', 'SUG'],
|
||||
'implications': ['imp', 'IMP'],
|
||||
}, ['sug'], ['imp']),
|
||||
# overlapping duplicate relations
|
||||
({
|
||||
'names': ['main'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['shared1', 'shared2'],
|
||||
'implications': ['SHARED1', 'SHARED2'],
|
||||
}, ['shared1', 'shared2'], ['shared1', 'shared2']),
|
||||
])
|
||||
def test_creating_new_suggestions_and_implications(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
result = tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['tag1'],
|
||||
'category': 'meta',
|
||||
'implications': ['tag2', 'tag3'],
|
||||
'suggestions': ['tag4', 'tag5'],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
assert result['tag']['implications'] == ['tag2', 'tag3']
|
||||
assert result['tag']['suggestions'] == ['tag4', 'tag5']
|
||||
tag = get_tag(session, 'tag1')
|
||||
assert_relations(tag.implications, ['tag2', 'tag3'])
|
||||
assert_relations(tag.suggestions, ['tag4', 'tag5'])
|
||||
test_ctx, input, expected_suggestions, expected_implications):
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input=input, user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert result['tag']['suggestions'] == expected_suggestions
|
||||
assert result['tag']['implications'] == expected_implications
|
||||
tag = get_tag(test_ctx.session, 'main')
|
||||
assert_relations(tag.suggestions, expected_suggestions)
|
||||
assert_relations(tag.implications, expected_implications)
|
||||
for name in ['main'] + expected_suggestions + expected_implications:
|
||||
assert get_tag(test_ctx.session, name) is not None
|
||||
|
||||
def test_duplicating_suggestions_and_implications(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
result = tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['tag1'],
|
||||
'category': 'meta',
|
||||
'implications': ['tag2', 'TAG2'],
|
||||
'suggestions': ['tag3', 'TAG3'],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
assert result['tag']['implications'] == ['tag2']
|
||||
assert result['tag']['suggestions'] == ['tag3']
|
||||
tag = get_tag(session, 'tag1')
|
||||
assert_relations(tag.implications, ['tag2'])
|
||||
assert_relations(tag.suggestions, ['tag3'])
|
||||
|
||||
def test_reusing_suggestions_and_implications(
|
||||
session,
|
||||
context_factory,
|
||||
user_factory,
|
||||
tag_factory,
|
||||
tag_list_api):
|
||||
session.add(tag_factory(names=['tag1', 'tag2'], category='meta'))
|
||||
session.add(tag_factory(names=['tag3'], category='meta'))
|
||||
session.commit()
|
||||
result = tag_list_api.post(
|
||||
context_factory(
|
||||
def test_reusing_suggestions_and_implications(test_ctx):
|
||||
test_ctx.session.add_all([
|
||||
test_ctx.tag_factory(names=['tag1', 'tag2'], category='meta'),
|
||||
test_ctx.tag_factory(names=['tag3'], category='meta'),
|
||||
])
|
||||
test_ctx.session.commit()
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['new'],
|
||||
'category': 'meta',
|
||||
'implications': ['tag1'],
|
||||
'suggestions': ['TAG2'],
|
||||
'implications': ['tag1'],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
assert result['tag']['implications'] == ['tag1']
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
# NOTE: it should export only the first name
|
||||
assert result['tag']['suggestions'] == ['tag1']
|
||||
tag = get_tag(session, 'new')
|
||||
assert_relations(tag.implications, ['tag1'])
|
||||
assert result['tag']['implications'] == ['tag1']
|
||||
tag = get_tag(test_ctx.session, 'new')
|
||||
assert_relations(tag.suggestions, ['tag1'])
|
||||
assert_relations(tag.implications, ['tag1'])
|
||||
|
||||
def test_tag_trying_to_imply_or_suggest_itself(
|
||||
session, context_factory, user_factory, tag_list_api):
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['tag1'],
|
||||
'category': 'meta',
|
||||
'implications': ['tag1'],
|
||||
'suggestions': [],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
with pytest.raises(errors.ValidationError):
|
||||
tag_list_api.post(
|
||||
context_factory(
|
||||
input={
|
||||
'names': ['tag1'],
|
||||
'category': 'meta',
|
||||
'implications': [],
|
||||
'suggestions': ['tag1'],
|
||||
},
|
||||
user=user_factory(rank='regular_user')))
|
||||
@pytest.mark.parametrize('input', [
|
||||
{
|
||||
'names': ['ok'],
|
||||
'category': 'meta',
|
||||
'suggestions': [],
|
||||
'implications': ['ok2', '!'],
|
||||
},
|
||||
{
|
||||
'names': ['ok'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['ok2', '!'],
|
||||
'implications': [],
|
||||
}
|
||||
])
|
||||
def test_trying_to_create_tag_with_invalid_relation(test_ctx, input):
|
||||
with pytest.raises(tags.InvalidNameError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input=input, user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert get_tag(test_ctx.session, 'ok') is None
|
||||
assert get_tag(test_ctx.session, 'ok2') is None
|
||||
assert get_tag(test_ctx.session, '!') is None
|
||||
|
||||
@pytest.mark.parametrize('input', [
|
||||
{
|
||||
'names': ['tag'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['tag'],
|
||||
'implications': [],
|
||||
},
|
||||
{
|
||||
'names': ['tag'],
|
||||
'category': 'meta',
|
||||
'suggestions': [],
|
||||
'implications': ['tag'],
|
||||
}
|
||||
])
|
||||
def test_tag_trying_to_relate_to_itself(test_ctx, input):
|
||||
assert get_tag(test_ctx.session, 'tag') is None
|
||||
with pytest.raises(tags.RelationError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input=input,
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert get_tag(test_ctx.session, 'tag') is None
|
||||
|
||||
# TODO: test bad privileges
|
||||
# TODO: test max length
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
import datetime
|
||||
import pytest
|
||||
from szurubooru import api, db, errors
|
||||
from szurubooru.util import misc, tags
|
||||
|
||||
def get_tag(session, name):
|
||||
return session.query(db.Tag) \
|
||||
.join(db.TagName) \
|
||||
.filter(db.TagName.name==name) \
|
||||
.first()
|
||||
|
||||
def assert_relations(relations, expected_tag_names):
|
||||
actual_names = [rel.child_tag.names[0].name for rel in relations]
|
||||
assert actual_names == expected_tag_names
|
||||
|
||||
@pytest.fixture
|
||||
def test_ctx(
|
||||
session, config_injector, context_factory, user_factory, tag_factory):
|
||||
config_injector({
|
||||
'tag_categories': ['meta', 'character', 'copyright'],
|
||||
'tag_name_regex': '^[^!]*$',
|
||||
'ranks': ['regular_user'],
|
||||
'privileges': {
|
||||
'tags:edit:names': 'regular_user',
|
||||
'tags:edit:category': 'regular_user',
|
||||
'tags:edit:suggestions': 'regular_user',
|
||||
'tags:edit:implications': 'regular_user',
|
||||
},
|
||||
})
|
||||
ret = misc.dotdict()
|
||||
ret.session = session
|
||||
ret.context_factory = context_factory
|
||||
ret.user_factory = user_factory
|
||||
ret.tag_factory = tag_factory
|
||||
ret.api = api.TagDetailApi()
|
||||
return ret
|
||||
|
||||
def test_simple_updating(test_ctx, fake_datetime):
|
||||
fake_datetime(datetime.datetime(1997, 12, 1))
|
||||
tag = test_ctx.tag_factory(names=['tag1', 'tag2'], category='meta')
|
||||
test_ctx.session.add(tag)
|
||||
test_ctx.session.commit()
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['tag3'],
|
||||
'category': 'character',
|
||||
},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag1')
|
||||
assert result == {
|
||||
'tag': {
|
||||
'names': ['tag3'],
|
||||
'category': 'character',
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
'creationTime': datetime.datetime(1996, 1, 1),
|
||||
'lastEditTime': datetime.datetime(1997, 12, 1),
|
||||
}
|
||||
}
|
||||
assert get_tag(test_ctx.session, 'tag1') is None
|
||||
assert get_tag(test_ctx.session, 'tag2') is None
|
||||
tag = get_tag(test_ctx.session, 'tag3')
|
||||
assert tag is not None
|
||||
assert [tag_name.name for tag_name in tag.names] == ['tag3']
|
||||
assert tag.category == 'character'
|
||||
assert tag.suggestions == []
|
||||
assert tag.implications == []
|
||||
|
||||
def test_trying_to_update_non_existing_tag(test_ctx):
|
||||
with pytest.raises(tags.TagNotFoundError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={'names': ['dummy']},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag1')
|
||||
|
||||
@pytest.mark.parametrize('dup_name', ['tag1', 'TAG1'])
|
||||
def test_reusing_own_name(test_ctx, dup_name):
|
||||
test_ctx.session.add(
|
||||
test_ctx.tag_factory(names=['tag1', 'tag2'], category='meta'))
|
||||
test_ctx.session.commit()
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={'names': [dup_name, 'tag3']},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag1')
|
||||
assert result['tag']['names'] == ['tag1', 'tag3']
|
||||
assert get_tag(test_ctx.session, 'tag2') is None
|
||||
tag1 = get_tag(test_ctx.session, 'tag1')
|
||||
tag2 = get_tag(test_ctx.session, 'tag3')
|
||||
assert tag1.tag_id == tag2.tag_id
|
||||
assert [name.name for name in tag1.names] == ['tag1', 'tag3']
|
||||
|
||||
def test_duplicating_names(test_ctx):
|
||||
test_ctx.session.add(
|
||||
test_ctx.tag_factory(names=['tag1', 'tag2'], category='meta'))
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={'names': ['tag3', 'TAG3']},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag1')
|
||||
assert result['tag']['names'] == ['tag3']
|
||||
assert get_tag(test_ctx.session, 'tag1') is None
|
||||
assert get_tag(test_ctx.session, 'tag2') is None
|
||||
tag = get_tag(test_ctx.session, 'tag3')
|
||||
assert tag is not None
|
||||
assert [tag_name.name for tag_name in tag.names] == ['tag3']
|
||||
|
||||
@pytest.mark.parametrize('input', [
|
||||
{'names': []},
|
||||
{'names': ['!']},
|
||||
])
|
||||
def test_trying_to_set_invalid_name(test_ctx, input):
|
||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag1'], category='meta'))
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(tags.InvalidNameError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input=input,
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag1')
|
||||
|
||||
@pytest.mark.parametrize('dup_name', ['tag1', 'TAG1', 'tag2', 'TAG2'])
|
||||
def test_trying_to_use_existing_name(test_ctx, dup_name):
|
||||
test_ctx.session.add_all([
|
||||
test_ctx.tag_factory(names=['tag1', 'tag2'], category='meta'),
|
||||
test_ctx.tag_factory(names=['tag3', 'tag4'], category='meta')])
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(tags.TagAlreadyExistsError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={'names': [dup_name]},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag3')
|
||||
|
||||
def test_trying_to_update_tag_with_invalid_category(test_ctx):
|
||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag1'], category='meta'))
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(tags.InvalidCategoryError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['ok'],
|
||||
'category': 'invalid',
|
||||
'suggestions': [],
|
||||
'implications': [],
|
||||
},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag1')
|
||||
|
||||
@pytest.mark.parametrize('input,expected_suggestions,expected_implications', [
|
||||
# new relations
|
||||
({
|
||||
'suggestions': ['sug1', 'sug2'],
|
||||
'implications': ['imp1', 'imp2'],
|
||||
}, ['sug1', 'sug2'], ['imp1', 'imp2']),
|
||||
# overlapping relations
|
||||
({
|
||||
'suggestions': ['sug', 'shared'],
|
||||
'implications': ['shared', 'imp'],
|
||||
}, ['sug', 'shared'], ['shared', 'imp']),
|
||||
# duplicate relations
|
||||
({
|
||||
'suggestions': ['sug', 'SUG'],
|
||||
'implications': ['imp', 'IMP'],
|
||||
}, ['sug'], ['imp']),
|
||||
# overlapping duplicate relations
|
||||
({
|
||||
'suggestions': ['shared1', 'shared2'],
|
||||
'implications': ['SHARED1', 'SHARED2'],
|
||||
}, ['shared1', 'shared2'], ['shared1', 'shared2']),
|
||||
])
|
||||
def test_updating_new_suggestions_and_implications(
|
||||
test_ctx, input, expected_suggestions, expected_implications):
|
||||
test_ctx.session.add(test_ctx.tag_factory(names=['main'], category='meta'))
|
||||
test_ctx.session.commit()
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input=input, user=test_ctx.user_factory(rank='regular_user')),
|
||||
'main')
|
||||
assert result['tag']['suggestions'] == expected_suggestions
|
||||
assert result['tag']['implications'] == expected_implications
|
||||
tag = get_tag(test_ctx.session, 'main')
|
||||
assert_relations(tag.suggestions, expected_suggestions)
|
||||
assert_relations(tag.implications, expected_implications)
|
||||
for name in ['main'] + expected_suggestions + expected_implications:
|
||||
assert get_tag(test_ctx.session, name) is not None
|
||||
|
||||
def test_reusing_suggestions_and_implications(test_ctx):
|
||||
test_ctx.session.add_all([
|
||||
test_ctx.tag_factory(names=['tag1', 'tag2'], category='meta'),
|
||||
test_ctx.tag_factory(names=['tag3'], category='meta'),
|
||||
test_ctx.tag_factory(names=['tag4'], category='meta'),
|
||||
])
|
||||
test_ctx.session.commit()
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'names': ['new'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['TAG2'],
|
||||
'implications': ['tag1'],
|
||||
},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag4')
|
||||
# NOTE: it should export only the first name
|
||||
assert result['tag']['suggestions'] == ['tag1']
|
||||
assert result['tag']['implications'] == ['tag1']
|
||||
tag = get_tag(test_ctx.session, 'new')
|
||||
assert_relations(tag.suggestions, ['tag1'])
|
||||
assert_relations(tag.implications, ['tag1'])
|
||||
|
||||
@pytest.mark.parametrize('input', [
|
||||
{'names': ['ok'], 'suggestions': ['ok2', '!']},
|
||||
{'names': ['ok'], 'implications': ['ok2', '!']},
|
||||
])
|
||||
def test_trying_to_update_tag_with_invalid_relation(test_ctx, input):
|
||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag'], category='meta'))
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(tags.InvalidNameError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input=input, user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag')
|
||||
test_ctx.session.rollback()
|
||||
assert get_tag(test_ctx.session, 'tag') is not None
|
||||
assert get_tag(test_ctx.session, '!') is None
|
||||
assert get_tag(test_ctx.session, 'ok') is None
|
||||
assert get_tag(test_ctx.session, 'ok2') is None
|
||||
|
||||
@pytest.mark.parametrize('input', [
|
||||
{
|
||||
'names': ['tag1'],
|
||||
'category': 'meta',
|
||||
'suggestions': ['tag1'],
|
||||
'implications': [],
|
||||
},
|
||||
{
|
||||
'names': ['tag1'],
|
||||
'category': 'meta',
|
||||
'suggestions': [],
|
||||
'implications': ['tag1'],
|
||||
}
|
||||
])
|
||||
def test_tag_trying_to_relate_to_itself(test_ctx, input):
|
||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag1'], category='meta'))
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(tags.RelationError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input=input, user=test_ctx.user_factory(rank='regular_user')),
|
||||
'tag1')
|
||||
|
||||
# TODO: test bad privileges
|
||||
# TODO: test max length
|
|
@ -1,9 +1,19 @@
|
|||
from datetime import datetime
|
||||
import datetime
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
from szurubooru import api, config, db
|
||||
from szurubooru.util import misc
|
||||
|
||||
@pytest.fixture
|
||||
def fake_datetime(monkeypatch):
|
||||
def injector(now):
|
||||
class mydatetime(datetime.datetime):
|
||||
@staticmethod
|
||||
def now(tz=None):
|
||||
return now
|
||||
monkeypatch.setattr(datetime, 'datetime', mydatetime)
|
||||
return injector
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
import logging
|
||||
|
@ -44,7 +54,7 @@ def user_factory():
|
|||
user.password_hash = 'dummy'
|
||||
user.email = 'dummy'
|
||||
user.rank = rank
|
||||
user.creation_time = datetime(1997, 1, 1)
|
||||
user.creation_time = datetime.datetime(1997, 1, 1)
|
||||
user.avatar_style = db.User.AVATAR_GRAVATAR
|
||||
return user
|
||||
return factory
|
||||
|
@ -55,6 +65,6 @@ def tag_factory():
|
|||
tag = db.Tag()
|
||||
tag.names = [db.TagName(name) for name in (names or ['dummy'])]
|
||||
tag.category = category
|
||||
tag.creation_time = datetime(1996, 1, 1)
|
||||
tag.creation_time = datetime.datetime(1996, 1, 1)
|
||||
return tag
|
||||
return factory
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
import pytest
|
||||
from datetime import datetime
|
||||
from szurubooru import errors
|
||||
from szurubooru.util import misc
|
||||
from datetime import datetime
|
||||
|
||||
dt = datetime
|
||||
|
||||
class FakeDatetime(datetime):
|
||||
@staticmethod
|
||||
def now(tz=None):
|
||||
return datetime(1997, 1, 2, 3, 4, 5, tzinfo=tz)
|
||||
|
||||
def test_parsing_empty_date_time():
|
||||
with pytest.raises(errors.ValidationError):
|
||||
misc.parse_time_range('')
|
||||
|
@ -25,8 +20,8 @@ def test_parsing_empty_date_time():
|
|||
('1999-2-06', (dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59))),
|
||||
('1999-02-06', (dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59))),
|
||||
])
|
||||
def test_parsing_date_time(input, output):
|
||||
misc.datetime.datetime = FakeDatetime
|
||||
def test_parsing_date_time(fake_datetime, input, output):
|
||||
fake_datetime(datetime(1997, 1, 2, 3, 4, 5))
|
||||
assert misc.parse_time_range(input) == output
|
||||
|
||||
@pytest.mark.parametrize('input,output', [
|
||||
|
|
|
@ -4,6 +4,48 @@ import sqlalchemy
|
|||
from szurubooru import config, db, errors
|
||||
from szurubooru.util import misc
|
||||
|
||||
class TagNotFoundError(errors.NotFoundError):
|
||||
def __init__(self, tag):
|
||||
super().__init__('Tag %r not found')
|
||||
|
||||
class TagAlreadyExistsError(errors.ValidationError):
|
||||
def __init__(self):
|
||||
super().__init__('One of names is already used by another tag.')
|
||||
|
||||
class InvalidNameError(errors.ValidationError):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
||||
class InvalidCategoryError(errors.ValidationError):
|
||||
def __init__(self, category, valid_categories):
|
||||
super().__init__(
|
||||
'Category %r is invalid. Valid categories: %r.' % (
|
||||
category, valid_categories))
|
||||
|
||||
class RelationError(errors.ValidationError):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
||||
def _verify_name_validity(name):
|
||||
name_regex = config.config['tag_name_regex']
|
||||
if not re.match(name_regex, name):
|
||||
raise InvalidNameError('Name must satisfy regex %r.' % name_regex)
|
||||
|
||||
def _get_plain_names(tag):
|
||||
return [tag_name.name for tag_name in tag.names]
|
||||
|
||||
def _lower_list(names):
|
||||
return [name.lower() for name in names]
|
||||
|
||||
def _check_name_intersection(names1, names2):
|
||||
return len(set(_lower_list(names1)).intersection(_lower_list(names2))) > 0
|
||||
|
||||
def get_by_name(session, name):
|
||||
return session.query(db.Tag) \
|
||||
.join(db.TagName) \
|
||||
.filter(db.TagName.name.ilike(name)) \
|
||||
.first()
|
||||
|
||||
def get_by_names(session, names):
|
||||
names = misc.icase_unique(names)
|
||||
if len(names) == 0:
|
||||
|
@ -14,17 +56,17 @@ def get_by_names(session, names):
|
|||
return session.query(db.Tag).join(db.TagName).filter(expr).all()
|
||||
|
||||
def get_or_create_by_names(session, names):
|
||||
names = misc.icase_unique(names)
|
||||
for name in names:
|
||||
_verify_name_validity(name)
|
||||
related_tags = get_by_names(session, names)
|
||||
new_tags = []
|
||||
for name in names:
|
||||
found = False
|
||||
for related_tag in related_tags:
|
||||
for tag_name in related_tag.names:
|
||||
if tag_name.name.lower() == name.lower():
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
if _check_name_intersection(_get_plain_names(related_tag), [name]):
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
new_tag = create_tag(
|
||||
session,
|
||||
|
@ -33,34 +75,29 @@ def get_or_create_by_names(session, names):
|
|||
suggestions=[],
|
||||
implications=[])
|
||||
session.add(new_tag)
|
||||
session.commit() # need to get id for use in association tables
|
||||
related_tags.append(new_tag)
|
||||
return related_tags
|
||||
new_tags.append(new_tag)
|
||||
return related_tags, new_tags
|
||||
|
||||
def create_tag(session, names, category, suggestions, implications):
|
||||
tag = db.Tag()
|
||||
tag.creation_time = datetime.datetime.now()
|
||||
update_category(tag, category)
|
||||
update_names(session, tag, names)
|
||||
update_category(session, tag, category)
|
||||
update_suggestions(session, tag, suggestions)
|
||||
update_implications(session, tag, implications)
|
||||
return tag
|
||||
|
||||
def update_category(tag, category):
|
||||
def update_category(session, tag, category):
|
||||
if not category in config.config['tag_categories']:
|
||||
raise errors.ValidationError(
|
||||
'Category must be either of %r.', config.config['tag_categories'])
|
||||
raise InvalidCategoryError(category, config.config['tag_categories'])
|
||||
tag.category = category
|
||||
|
||||
def update_names(session, tag, names):
|
||||
names = misc.icase_unique(names)
|
||||
if not len(names):
|
||||
raise errors.ValidationError('At least one name must be specified.')
|
||||
raise InvalidNameError('At least one name must be specified.')
|
||||
for name in names:
|
||||
name_regex = config.config['tag_name_regex']
|
||||
if not re.match(name_regex, name):
|
||||
raise errors.ValidationError(
|
||||
'Name must satisfy regex %r.' % name_regex)
|
||||
_verify_name_validity(name)
|
||||
expr = sqlalchemy.sql.false()
|
||||
for name in names:
|
||||
expr = expr | db.TagName.name.ilike(name)
|
||||
|
@ -68,22 +105,33 @@ def update_names(session, tag, names):
|
|||
expr = expr & (db.TagName.tag_id != tag.tag_id)
|
||||
existing_tags = session.query(db.TagName).filter(expr).all()
|
||||
if len(existing_tags):
|
||||
raise errors.ValidationError(
|
||||
'One of names is already used by another tag.')
|
||||
tag.names = []
|
||||
raise TagAlreadyExistsError()
|
||||
tag_names_to_remove = []
|
||||
for tag_name in tag.names:
|
||||
if tag_name.name.lower() not in [name.lower() for name in names]:
|
||||
tag_names_to_remove.append(tag_name)
|
||||
for tag_name in tag_names_to_remove:
|
||||
tag.names.remove(tag_name)
|
||||
for name in names:
|
||||
tag_name = db.TagName(name)
|
||||
session.add(tag_name)
|
||||
tag.names.append(tag_name)
|
||||
if name.lower() not in [tag_name.name.lower() for tag_name in tag.names]:
|
||||
tag_name = db.TagName(name)
|
||||
session.add(tag_name)
|
||||
tag.names.append(tag_name)
|
||||
|
||||
def update_implications(session, tag, relations):
|
||||
related_tags = get_or_create_by_names(session, relations)
|
||||
if _check_name_intersection(_get_plain_names(tag), relations):
|
||||
raise RelationError('Tag cannot imply itself.')
|
||||
related_tags, new_tags = get_or_create_by_names(session, relations)
|
||||
session.flush()
|
||||
tag.implications = [
|
||||
db.TagImplication(tag.tag_id, other_tag.tag_id) \
|
||||
for other_tag in related_tags]
|
||||
for other_tag in related_tags + new_tags]
|
||||
|
||||
def update_suggestions(session, tag, relations):
|
||||
related_tags = get_or_create_by_names(session, relations)
|
||||
if _check_name_intersection(_get_plain_names(tag), relations):
|
||||
raise RelationError('Tag cannot suggest itself.')
|
||||
related_tags, new_tags = get_or_create_by_names(session, relations)
|
||||
session.flush()
|
||||
tag.suggestions = [
|
||||
db.TagSuggestion(tag.tag_id, other_tag.tag_id) \
|
||||
for other_tag in related_tags]
|
||||
for other_tag in related_tags + new_tags]
|
||||
|
|
Loading…
Reference in New Issue