server/tags: introduce tag category API
This commit is contained in:
parent
83784c5e76
commit
2fba374e65
133
API.md
133
API.md
|
@ -92,28 +92,151 @@ data.
|
|||
|
||||
|
||||
## Listing tag categories
|
||||
- **Request**
|
||||
|
||||
Not implemented yet.
|
||||
`GET /tag-categories`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"tagCategories": [
|
||||
<tag-category>,
|
||||
<tag-category>,
|
||||
<tag-category>
|
||||
]
|
||||
}
|
||||
```
|
||||
...where `<tag-category>` is a [tag category resource](#tag-category).
|
||||
|
||||
- **Errors**
|
||||
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Lists all tag categories. Doesn't support paging.
|
||||
|
||||
|
||||
## Creating tag category
|
||||
- **Request**
|
||||
|
||||
Not implemented yet.
|
||||
`POST /tag-categories`
|
||||
|
||||
- **Input**
|
||||
|
||||
```json5
|
||||
{
|
||||
"name": <name>,
|
||||
"color": <color>
|
||||
}
|
||||
```
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"tagCategory": <tag-category>
|
||||
}
|
||||
```
|
||||
...where `<tag-category>` is a [tag category resource](#tag-category).
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the name is used by an existing tag category (names are case insensitive)
|
||||
- the name is invalid or missing
|
||||
- the color is invalid or missing
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Creates a new tag category using specified parameters. Name must match
|
||||
`tag_category_name_regex` from server's configuration.
|
||||
|
||||
|
||||
## Updating tag category
|
||||
- **Request**
|
||||
|
||||
Not implemented yet.
|
||||
`PUT /tag-category/<name>`
|
||||
|
||||
- **Input**
|
||||
|
||||
```json5
|
||||
{
|
||||
"name": <name>, // optional
|
||||
"color": <color>, // optional
|
||||
}
|
||||
```
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"tagCategory": <tag-category>
|
||||
}
|
||||
```
|
||||
...where `<tag-category>` is a [tag category resource](#tag-category).
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the tag category does not exist
|
||||
- the name is used by an existing tag category (names are case insensitive)
|
||||
- the name is invalid
|
||||
- the color is invalid
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Updates an existing tag category using specified parameters. Name must
|
||||
match `tag_category_name_regex` from server's configuration. All fields are
|
||||
optional - update concerns only provided fields.
|
||||
|
||||
|
||||
## Getting tag category
|
||||
- **Request**
|
||||
|
||||
Not implemented yet.
|
||||
`GET /tag-category/<name>`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"tagCategory": <tag-category>
|
||||
}
|
||||
```
|
||||
...where `<tag-category>` is a [tag category resource](#tag-category).
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the tag category does not exist
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Retrieves information about an existing tag category.
|
||||
|
||||
|
||||
## Deleting tag category
|
||||
- **Request**
|
||||
|
||||
Not implemented yet.
|
||||
`DELETE /tag-category/<name>`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{}
|
||||
```
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the tag category does not exist
|
||||
- the tag category is used by some tags
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Deletes existing tag category. The tag category to be deleted must have no
|
||||
usages.
|
||||
|
||||
|
||||
## Listing tags
|
||||
|
|
|
@ -37,6 +37,7 @@ limits:
|
|||
max_comment_length: 5000
|
||||
|
||||
tag_name_regex: ^:?[a-zA-Z0-9_-]+$
|
||||
tag_category_name_regex: ^.{1,}$
|
||||
|
||||
# changing ranks after deployment may require manual tweaks to the database.
|
||||
ranks:
|
||||
|
@ -103,6 +104,12 @@ privileges:
|
|||
'tags:merge': mod
|
||||
'tags:delete': mod
|
||||
|
||||
'tag_categories:create': mod
|
||||
'tag_categories:edit:name': mod
|
||||
'tag_categories:edit:color': mod
|
||||
'tag_categories:list': anonymous
|
||||
'tag_categories:delete': mod
|
||||
|
||||
'comments:create': regular_user
|
||||
'comments:delete:any': mod
|
||||
'comments:delete:own': regular_user
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
from szurubooru.api.password_reset_api import PasswordResetApi
|
||||
from szurubooru.api.user_api import UserListApi, UserDetailApi
|
||||
from szurubooru.api.tag_api import TagListApi, TagDetailApi
|
||||
from szurubooru.api.tag_category_api import TagCategoryListApi, TagCategoryDetailApi
|
||||
from szurubooru.api.context import Context, Request
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
from szurubooru.util import auth, tags, tag_categories
|
||||
from szurubooru.api.base_api import BaseApi
|
||||
|
||||
def _serialize_category(category):
|
||||
return {
|
||||
'name': category.name,
|
||||
'color': category.color,
|
||||
}
|
||||
|
||||
class TagCategoryListApi(BaseApi):
|
||||
def get(self, ctx):
|
||||
auth.verify_privilege(ctx.user, 'tag_categories:list')
|
||||
categories = tag_categories.get_all_categories()
|
||||
return {
|
||||
'tagCategories': [
|
||||
_serialize_category(category) for category in categories],
|
||||
}
|
||||
|
||||
def post(self, ctx):
|
||||
auth.verify_privilege(ctx.user, 'tag_categories:create')
|
||||
name = ctx.get_param_as_string('name', required=True)
|
||||
color = ctx.get_param_as_string('color', required=True)
|
||||
category = tag_categories.create_category(name, color)
|
||||
ctx.session.add(category)
|
||||
ctx.session.commit()
|
||||
tags.export_to_json()
|
||||
return {'tagCategory': _serialize_category(category)}
|
||||
|
||||
class TagCategoryDetailApi(BaseApi):
|
||||
def get(self, ctx, category_name):
|
||||
auth.verify_privilege(ctx.user, 'tag_categories:view')
|
||||
category = tag_categories.get_category_by_name(category_name)
|
||||
if not category:
|
||||
raise tag_categories.TagCategoryNotFoundError(
|
||||
'Tag category %r not found.' % category_name)
|
||||
return {'tagCategory': _serialize_category(category)}
|
||||
|
||||
def put(self, ctx, category_name):
|
||||
category = tag_categories.get_category_by_name(category_name)
|
||||
if not category:
|
||||
raise tag_categories.TagCategoryNotFoundError(
|
||||
'Tag category %r not found.' % category_name)
|
||||
if ctx.has_param('name'):
|
||||
auth.verify_privilege(ctx.user, 'tag_categories:edit:name')
|
||||
tag_categories.update_name(
|
||||
category, ctx.get_param_as_string('name'))
|
||||
if ctx.has_param('color'):
|
||||
auth.verify_privilege(ctx.user, 'tag_categories:edit:color')
|
||||
tag_categories.update_color(
|
||||
category, ctx.get_param_as_string('color'))
|
||||
ctx.session.commit()
|
||||
tags.export_to_json()
|
||||
return {'tagCategory': _serialize_category(category)}
|
||||
|
||||
def delete(self, ctx, category_name):
|
||||
category = tag_categories.get_category_by_name(category_name)
|
||||
if not category:
|
||||
raise tag_categories.TagCategoryNotFoundError(
|
||||
'Tag category %r not found.' % category_name)
|
||||
if category.tag_count > 0:
|
||||
raise tag_categories.TagCategoryIsInUseError(
|
||||
'Tag category has some usages and cannot be deleted. ' +
|
||||
'Please remove this category from relevant tags first..')
|
||||
auth.verify_privilege(ctx.user, 'tag_categories:delete')
|
||||
ctx.session.delete(category)
|
||||
ctx.session.commit()
|
||||
tags.export_to_json()
|
||||
return {}
|
|
@ -36,6 +36,8 @@ def create_app():
|
|||
|
||||
user_list_api = api.UserListApi()
|
||||
user_detail_api = api.UserDetailApi()
|
||||
tag_category_list_api = api.TagCategoryListApi()
|
||||
tag_category_detail_api = api.TagCategoryDetailApi()
|
||||
tag_list_api = api.TagListApi()
|
||||
tag_detail_api = api.TagDetailApi()
|
||||
password_reset_api = api.PasswordResetApi()
|
||||
|
@ -49,6 +51,8 @@ def create_app():
|
|||
|
||||
app.add_route('/users/', user_list_api)
|
||||
app.add_route('/user/{user_name}', user_detail_api)
|
||||
app.add_route('/tag-categories/', tag_category_list_api)
|
||||
app.add_route('/tag-category/{category_name}', tag_category_detail_api)
|
||||
app.add_route('/tags/', tag_list_api)
|
||||
app.add_route('/tag/{tag_name}', tag_detail_api)
|
||||
app.add_route('/password-reset/{user_name}', password_reset_api)
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy import Column, Integer, String, table
|
||||
from sqlalchemy.orm import column_property
|
||||
from sqlalchemy.sql.expression import func, select
|
||||
from szurubooru.db.base import Base
|
||||
from szurubooru.db.tag import Tag
|
||||
|
||||
class TagCategory(Base):
|
||||
__tablename__ = 'tag_category'
|
||||
|
@ -8,5 +11,10 @@ class TagCategory(Base):
|
|||
name = Column('name', String(32), nullable=False)
|
||||
color = Column('color', String(32), nullable=False, default='#000000')
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, name=None):
|
||||
self.name = name
|
||||
|
||||
tag_count = column_property(
|
||||
select([func.count('Tag.tag_id')]) \
|
||||
.where(Tag.category_id == tag_category_id) \
|
||||
.correlate(table('TagCategory')))
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import os
|
||||
import pytest
|
||||
from szurubooru import api, config, db, errors
|
||||
from szurubooru.util import misc, tag_categories
|
||||
|
||||
@pytest.fixture
|
||||
def test_ctx(tmpdir, config_injector, context_factory, user_factory):
|
||||
config_injector({
|
||||
'data_dir': str(tmpdir),
|
||||
'tag_category_name_regex': '^[^!]+$',
|
||||
'ranks': ['anonymous', 'regular_user'],
|
||||
'privileges': {'tag_categories:create': 'regular_user'},
|
||||
})
|
||||
ret = misc.dotdict()
|
||||
ret.context_factory = context_factory
|
||||
ret.user_factory = user_factory
|
||||
ret.api = api.TagCategoryListApi()
|
||||
return ret
|
||||
|
||||
def test_creating_category(test_ctx):
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={'name': 'meta', 'color': 'black'},
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert result == {
|
||||
'tagCategory': {'name': 'meta', 'color': 'black'},
|
||||
}
|
||||
category = db.session.query(db.TagCategory).one()
|
||||
assert category.name == 'meta'
|
||||
assert category.color == 'black'
|
||||
assert category.tag_count == 0
|
||||
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
||||
|
||||
@pytest.mark.parametrize('input', [
|
||||
{'name': None},
|
||||
{'name': ''},
|
||||
{'name': '!bad'},
|
||||
{'color': None},
|
||||
{'color': ''},
|
||||
])
|
||||
def test_invalid_inputs(test_ctx, input):
|
||||
real_input = {
|
||||
'name': 'okay',
|
||||
'color': 'okay',
|
||||
}
|
||||
for key, value in input.items():
|
||||
real_input[key] = value
|
||||
with pytest.raises(errors.ValidationError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input=real_input,
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
|
||||
@pytest.mark.parametrize('field', ['name', 'color'])
|
||||
def test_missing_mandatory_field(test_ctx, field):
|
||||
input = {
|
||||
'name': 'meta',
|
||||
'color': 'black',
|
||||
}
|
||||
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')))
|
||||
|
||||
def test_trying_to_use_existing_name(test_ctx):
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={'name': 'meta', 'color': 'black'},
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
with pytest.raises(tag_categories.TagCategoryAlreadyExistsError):
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={'name': 'meta', 'color': 'black'},
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
with pytest.raises(tag_categories.TagCategoryAlreadyExistsError):
|
||||
result = test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={'name': 'META', 'color': 'black'},
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
|
||||
def test_trying_to_create_tag_with_invalid_color(test_ctx):
|
||||
with pytest.raises(tag_categories.InvalidTagCategoryColorError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={'name': 'meta', 'color': 'a' * 100},
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert db.session.query(db.TagCategory).filter_by(name='meta').count() == 0
|
||||
|
||||
def test_trying_to_create_tag_without_privileges(test_ctx):
|
||||
with pytest.raises(errors.AuthError):
|
||||
test_ctx.api.post(
|
||||
test_ctx.context_factory(
|
||||
input={'name': 'meta', 'colro': 'black'},
|
||||
user=test_ctx.user_factory(rank='anonymous')))
|
|
@ -0,0 +1,69 @@
|
|||
import pytest
|
||||
import os
|
||||
from datetime import datetime
|
||||
from szurubooru import api, config, db, errors
|
||||
from szurubooru.util import misc, tags, tag_categories
|
||||
|
||||
@pytest.fixture
|
||||
def test_ctx(
|
||||
tmpdir,
|
||||
config_injector,
|
||||
context_factory,
|
||||
tag_factory,
|
||||
tag_category_factory,
|
||||
user_factory):
|
||||
config_injector({
|
||||
'data_dir': str(tmpdir),
|
||||
'privileges': {
|
||||
'tag_categories:delete': 'regular_user',
|
||||
},
|
||||
'ranks': ['anonymous', 'regular_user'],
|
||||
})
|
||||
ret = misc.dotdict()
|
||||
ret.context_factory = context_factory
|
||||
ret.user_factory = user_factory
|
||||
ret.tag_factory = tag_factory
|
||||
ret.tag_category_factory = tag_category_factory
|
||||
ret.api = api.TagCategoryDetailApi()
|
||||
return ret
|
||||
|
||||
def test_deleting(test_ctx):
|
||||
db.session().add(test_ctx.tag_category_factory(name='category'))
|
||||
db.session().commit()
|
||||
result = test_ctx.api.delete(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'category')
|
||||
assert result == {}
|
||||
assert db.session().query(db.TagCategory).count() == 0
|
||||
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
||||
|
||||
def test_deleting_with_usages(test_ctx, tag_factory):
|
||||
category = test_ctx.tag_category_factory(name='category')
|
||||
db.session().add(category)
|
||||
db.session().flush()
|
||||
tag = test_ctx.tag_factory(names=['tag'], category=category)
|
||||
db.session().add(tag)
|
||||
db.session().commit()
|
||||
with pytest.raises(tag_categories.TagCategoryIsInUseError):
|
||||
test_ctx.api.delete(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'category')
|
||||
assert db.session().query(db.TagCategory).count() == 1
|
||||
|
||||
def test_deleting_non_existing(test_ctx):
|
||||
with pytest.raises(tag_categories.TagCategoryNotFoundError):
|
||||
test_ctx.api.delete(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='regular_user')), 'bad')
|
||||
|
||||
def test_deleting_without_privileges(test_ctx):
|
||||
db.session().add(test_ctx.tag_category_factory(name='category'))
|
||||
db.session().commit()
|
||||
with pytest.raises(errors.AuthError):
|
||||
test_ctx.api.delete(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='anonymous')),
|
||||
'category')
|
||||
assert db.session().query(db.TagCategory).count() == 1
|
|
@ -0,0 +1,61 @@
|
|||
import datetime
|
||||
import pytest
|
||||
from szurubooru import api, db, errors
|
||||
from szurubooru.util import misc, tag_categories
|
||||
|
||||
@pytest.fixture
|
||||
def test_ctx(
|
||||
context_factory, config_injector, user_factory, tag_category_factory):
|
||||
config_injector({
|
||||
'privileges': {
|
||||
'tag_categories:list': 'regular_user',
|
||||
'tag_categories:view': 'regular_user',
|
||||
},
|
||||
'thumbnails': {'avatar_width': 200},
|
||||
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
|
||||
'rank_names': {'regular_user': 'Peasant'},
|
||||
})
|
||||
ret = misc.dotdict()
|
||||
ret.context_factory = context_factory
|
||||
ret.user_factory = user_factory
|
||||
ret.tag_category_factory = tag_category_factory
|
||||
ret.list_api = api.TagCategoryListApi()
|
||||
ret.detail_api = api.TagCategoryDetailApi()
|
||||
return ret
|
||||
|
||||
def test_retrieving_multiple(test_ctx):
|
||||
db.session().add_all([
|
||||
test_ctx.tag_category_factory(name='c1'),
|
||||
test_ctx.tag_category_factory(name='c2'),
|
||||
])
|
||||
result = test_ctx.list_api.get(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='regular_user')))
|
||||
assert [cat['name'] for cat in result['tagCategories']] == ['c1', 'c2']
|
||||
|
||||
def test_retrieving_single(test_ctx):
|
||||
db.session().add(test_ctx.tag_category_factory(name='cat'))
|
||||
result = test_ctx.detail_api.get(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'cat')
|
||||
assert result == {
|
||||
'tagCategory': {
|
||||
'name': 'cat',
|
||||
'color': 'dummy',
|
||||
}
|
||||
}
|
||||
|
||||
def test_retrieving_non_existing(test_ctx):
|
||||
with pytest.raises(tag_categories.TagCategoryNotFoundError):
|
||||
test_ctx.detail_api.get(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'-')
|
||||
|
||||
def test_retrieving_single_without_privileges(test_ctx):
|
||||
with pytest.raises(errors.AuthError):
|
||||
test_ctx.detail_api.get(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='anonymous')),
|
||||
'-')
|
|
@ -0,0 +1,133 @@
|
|||
import os
|
||||
import pytest
|
||||
from szurubooru import api, config, db, errors
|
||||
from szurubooru.util import misc, tag_categories
|
||||
|
||||
@pytest.fixture
|
||||
def test_ctx(
|
||||
tmpdir,
|
||||
config_injector,
|
||||
context_factory,
|
||||
user_factory,
|
||||
tag_category_factory):
|
||||
config_injector({
|
||||
'data_dir': str(tmpdir),
|
||||
'tag_category_name_regex': '^[^!]*$',
|
||||
'ranks': ['anonymous', 'regular_user'],
|
||||
'privileges': {
|
||||
'tag_categories:edit:name': 'regular_user',
|
||||
'tag_categories:edit:color': 'regular_user',
|
||||
},
|
||||
})
|
||||
ret = misc.dotdict()
|
||||
ret.context_factory = context_factory
|
||||
ret.user_factory = user_factory
|
||||
ret.tag_category_factory = tag_category_factory
|
||||
ret.api = api.TagCategoryDetailApi()
|
||||
return ret
|
||||
|
||||
def test_simple_updating(test_ctx):
|
||||
category = test_ctx.tag_category_factory(name='name', color='black')
|
||||
db.session().add(category)
|
||||
db.session().commit()
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={
|
||||
'name': 'changed',
|
||||
'color': 'white',
|
||||
},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'name')
|
||||
assert result == {
|
||||
'tagCategory': {
|
||||
'name': 'changed',
|
||||
'color': 'white',
|
||||
}
|
||||
}
|
||||
assert tag_categories.get_category_by_name('name') is None
|
||||
category = tag_categories.get_category_by_name('changed')
|
||||
assert category is not None
|
||||
assert category.name == 'changed'
|
||||
assert category.color == 'white'
|
||||
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
||||
|
||||
@pytest.mark.parametrize('input', [
|
||||
{'name': None},
|
||||
{'name': ''},
|
||||
{'name': '!bad'},
|
||||
{'color': None},
|
||||
{'color': ''},
|
||||
])
|
||||
def test_invalid_inputs(test_ctx, input):
|
||||
db.session().add(test_ctx.tag_category_factory(name='meta', color='black'))
|
||||
db.session().commit()
|
||||
with pytest.raises(tag_categories.InvalidTagCategoryNameError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input=input,
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'meta')
|
||||
|
||||
@pytest.mark.parametrize('field', ['name', 'color'])
|
||||
def test_missing_optional_field(test_ctx, tmpdir, field):
|
||||
db.session().add(test_ctx.tag_category_factory(name='name', color='black'))
|
||||
db.session().commit()
|
||||
input = {
|
||||
'name': 'changed',
|
||||
'color': 'white',
|
||||
}
|
||||
del input[field]
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input=input,
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'name')
|
||||
assert result is not None
|
||||
|
||||
def test_trying_to_update_non_existing(test_ctx):
|
||||
with pytest.raises(tag_categories.TagCategoryNotFoundError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={'name': ['dummy']},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'bad')
|
||||
|
||||
@pytest.mark.parametrize('new_name', ['cat', 'CAT'])
|
||||
def test_reusing_own_name(test_ctx, new_name):
|
||||
db.session().add(test_ctx.tag_category_factory(name='cat', color='black'))
|
||||
db.session().commit()
|
||||
result = test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={'name': new_name},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'cat')
|
||||
assert result['tagCategory']['name'] == new_name
|
||||
category = tag_categories.get_category_by_name('cat')
|
||||
assert category.name == new_name
|
||||
|
||||
@pytest.mark.parametrize('dup_name', ['cat1', 'CAT1'])
|
||||
def test_trying_to_use_existing_name(test_ctx, dup_name):
|
||||
db.session().add_all([
|
||||
test_ctx.tag_category_factory(name='cat1', color='black'),
|
||||
test_ctx.tag_category_factory(name='cat2', color='black')])
|
||||
db.session().commit()
|
||||
with pytest.raises(tag_categories.TagCategoryAlreadyExistsError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input={'name': dup_name},
|
||||
user=test_ctx.user_factory(rank='regular_user')),
|
||||
'cat2')
|
||||
|
||||
@pytest.mark.parametrize('input', [
|
||||
{'name': 'whatever'},
|
||||
{'color': 'whatever'},
|
||||
])
|
||||
def test_trying_to_update_tag_without_privileges(test_ctx, input):
|
||||
db.session().add(test_ctx.tag_category_factory(name='dummy'))
|
||||
db.session().commit()
|
||||
with pytest.raises(errors.AuthError):
|
||||
test_ctx.api.put(
|
||||
test_ctx.context_factory(
|
||||
input=input,
|
||||
user=test_ctx.user_factory(rank='anonymous')),
|
||||
'dummy')
|
|
@ -38,16 +38,6 @@ def test_deleting(test_ctx):
|
|||
assert test_ctx.session.query(db.Tag).count() == 0
|
||||
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
||||
|
||||
def test_deleting_without_privileges(test_ctx):
|
||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(errors.AuthError):
|
||||
test_ctx.api.delete(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='anonymous')),
|
||||
'tag')
|
||||
assert test_ctx.session.query(db.Tag).count() == 1
|
||||
|
||||
def test_deleting_with_usages(test_ctx, post_factory):
|
||||
tag = test_ctx.tag_factory(names=['tag'])
|
||||
post = post_factory()
|
||||
|
@ -66,3 +56,13 @@ def test_deleting_non_existing(test_ctx):
|
|||
test_ctx.api.delete(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='regular_user')), 'bad')
|
||||
|
||||
def test_deleting_without_privileges(test_ctx):
|
||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
||||
test_ctx.session.commit()
|
||||
with pytest.raises(errors.AuthError):
|
||||
test_ctx.api.delete(
|
||||
test_ctx.context_factory(
|
||||
user=test_ctx.user_factory(rank='anonymous')),
|
||||
'tag')
|
||||
assert test_ctx.session.query(db.Tag).count() == 1
|
||||
|
|
|
@ -66,11 +66,21 @@ def user_factory():
|
|||
return user
|
||||
return factory
|
||||
|
||||
@pytest.fixture
|
||||
def tag_category_factory(session):
|
||||
def factory(name='dummy', color='dummy'):
|
||||
category = db.TagCategory()
|
||||
category.name = name
|
||||
category.color = color
|
||||
return category
|
||||
return factory
|
||||
|
||||
@pytest.fixture
|
||||
def tag_factory(session):
|
||||
def factory(names=None, category_name='dummy'):
|
||||
category = db.TagCategory(category_name)
|
||||
session.add(category)
|
||||
def factory(names=None, category=None, category_name='dummy'):
|
||||
if not category:
|
||||
category = db.TagCategory(category_name)
|
||||
session.add(category)
|
||||
tag = db.Tag()
|
||||
tag.names = [db.TagName(name) for name in (names or [get_unique_name()])]
|
||||
tag.category = category
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import re
|
||||
from szurubooru import config, db, errors
|
||||
from szurubooru.util import misc
|
||||
|
||||
class TagCategoryNotFoundError(errors.NotFoundError): pass
|
||||
class TagCategoryAlreadyExistsError(errors.ValidationError): pass
|
||||
class TagCategoryIsInUseError(errors.ValidationError): pass
|
||||
class InvalidTagCategoryNameError(errors.ValidationError): pass
|
||||
class InvalidTagCategoryColorError(errors.ValidationError): pass
|
||||
|
||||
def _verify_name_validity(name):
|
||||
name_regex = config.config['tag_category_name_regex']
|
||||
if not re.match(name_regex, name):
|
||||
raise InvalidTagCategoryNameError(
|
||||
'Name must satisfy regex %r.' % name_regex)
|
||||
|
||||
def create_category(name, color):
|
||||
category = db.TagCategory()
|
||||
update_name(category, name)
|
||||
update_color(category, color)
|
||||
return category
|
||||
|
||||
def update_name(category, name):
|
||||
if not name:
|
||||
raise InvalidTagCategoryNameError('Name cannot be empty.')
|
||||
expr = db.TagCategory.name.ilike(name)
|
||||
if category.tag_category_id:
|
||||
expr = expr & (db.TagCategory.tag_category_id != category.tag_category_id)
|
||||
already_exists = db.session().query(db.TagCategory).filter(expr).count() > 0
|
||||
if already_exists:
|
||||
raise TagCategoryAlreadyExistsError(
|
||||
'A category with this name already exists.')
|
||||
if misc.value_exceeds_column_size(name, db.TagCategory.name):
|
||||
raise InvalidTagCategoryNameError('Name is too long.')
|
||||
_verify_name_validity(name)
|
||||
category.name = name
|
||||
|
||||
def update_color(category, color):
|
||||
if not color:
|
||||
raise InvalidTagCategoryNameError('Color cannot be empty.')
|
||||
if misc.value_exceeds_column_size(color, db.TagCategory.color):
|
||||
raise InvalidTagCategoryColorError('Color is too long.')
|
||||
category.color = color
|
||||
|
||||
def get_category_by_name(name):
|
||||
return db.session.query(db.TagCategory) \
|
||||
.filter(db.TagCategory.name.ilike(name)) \
|
||||
.first()
|
||||
|
||||
def get_all_category_names():
|
||||
return [row[0] for row in db.session.query(db.TagCategory.name).all()]
|
||||
|
||||
def get_all_categories():
|
||||
return db.session.query(db.TagCategory).all()
|
||||
|
||||
def get_default_category():
|
||||
return db.session().query(db.TagCategory) \
|
||||
.order_by(db.TagCategory.tag_category_id.asc()) \
|
||||
.limit(1) \
|
||||
.one()
|
|
@ -4,7 +4,7 @@ import os
|
|||
import re
|
||||
import sqlalchemy
|
||||
from szurubooru import config, db, errors
|
||||
from szurubooru.util import misc
|
||||
from szurubooru.util import misc, tag_categories
|
||||
|
||||
class TagNotFoundError(errors.NotFoundError): pass
|
||||
class TagAlreadyExistsError(errors.ValidationError): pass
|
||||
|
@ -51,12 +51,6 @@ def get_tag_by_name(name):
|
|||
.filter(db.TagName.name.ilike(name)) \
|
||||
.first()
|
||||
|
||||
def get_default_category():
|
||||
return db.session().query(db.TagCategory) \
|
||||
.order_by(db.TagCategory.tag_category_id.asc()) \
|
||||
.limit(1) \
|
||||
.one()
|
||||
|
||||
def get_tags_by_names(names):
|
||||
names = misc.icase_unique(names)
|
||||
if len(names) == 0:
|
||||
|
@ -81,7 +75,7 @@ def get_or_create_tags_by_names(names):
|
|||
if not found:
|
||||
new_tag = create_tag(
|
||||
names=[name],
|
||||
category_name=get_default_category().name,
|
||||
category_name=tag_categories.get_default_category().name,
|
||||
suggestions=[],
|
||||
implications=[])
|
||||
db.session().add(new_tag)
|
||||
|
@ -103,8 +97,7 @@ def update_category_name(tag, category_name):
|
|||
.filter(db.TagCategory.name == category_name) \
|
||||
.first()
|
||||
if not category:
|
||||
category_names = [
|
||||
name[0] for name in session.query(db.TagCategory.name).all()]
|
||||
category_names = tag_categories.get_all_category_names()
|
||||
raise InvalidTagCategoryError(
|
||||
'Category %r is invalid. Valid categories: %r.' % (
|
||||
category_name, category_names))
|
||||
|
|
Loading…
Reference in New Issue