server/tags: add tag updating

This commit is contained in:
rr- 2016-04-16 10:57:42 +02:00
parent 018dedcffd
commit adecdd4cd9
7 changed files with 615 additions and 212 deletions

55
API.md
View File

@ -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"
}
```

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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', [

View File

@ -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]