server/tags: add listing tag siblings

This commit is contained in:
rr- 2016-04-20 21:31:46 +02:00
parent 747c730688
commit 97bd935bb6
7 changed files with 170 additions and 6 deletions

38
API.md
View file

@ -26,6 +26,7 @@
- [Getting tag](#getting-tag)
- [Deleting tag](#deleting-tag)
- [Merging tags](#merging-tags)
- [Listing tag siblings](#listing-tag-siblings)
- Users
- [Listing users](#listing-users)
- [Creating user](#creating-user)
@ -116,7 +117,7 @@ data.
- **Description**
Lists all tag categories. Doesn't support paging.
Lists all tag categories. Doesn't use paging.
**Note**: independently, the server exports current tag category list
snapshots to the data directory under `tags.json` name. Its purpose is to
@ -552,6 +553,41 @@ data.
exception of the set of posts it's used in.
## Listing tag siblings
- **Request**
`GET /tag-siblings/<name>`
- **Output**
```json5
{
"siblings": [
{
"tag": <tag>,
"occurrences": 2
},
{
"tag": <tag>,
"occurrences": 1
}
]
}
```
...where `<tag>` is a [tag resource](#tag).
- **Errors**
- privileges are too low
- **Description**
Lists siblings of given tag, e.g. tags that were used in the same posts as
the given tag. `occurrences` field signifies how many times a given sibling
appears with given tag. Results are sorted by occurrences count and the
list is truncated to the first 50 elements. Doesn't use paging.
## Listing users
- **Request**

View file

@ -2,6 +2,6 @@
from szurubooru.api.password_reset_api import PasswordResetApi
from szurubooru.api.user_api import UserListApi, UserDetailApi
from szurubooru.api.tag_api import TagListApi, TagDetailApi, TagMergingApi
from szurubooru.api.tag_api import TagListApi, TagDetailApi, TagMergeApi, TagSiblingsApi
from szurubooru.api.tag_category_api import TagCategoryListApi, TagCategoryDetailApi
from szurubooru.api.context import Context, Request

View file

@ -110,7 +110,7 @@ class TagDetailApi(BaseApi):
tags.export_to_json()
return {}
class TagMergingApi(BaseApi):
class TagMergeApi(BaseApi):
def post(self, ctx):
source_tag_name = ctx.get_param_as_string('remove', required=True) or ''
target_tag_name = ctx.get_param_as_string('merge-to', required=True) or ''
@ -131,3 +131,18 @@ class TagMergingApi(BaseApi):
ctx.session.commit()
tags.export_to_json()
return _serialize_tag_with_details(target_tag)
class TagSiblingsApi(BaseApi):
def get(self, ctx, tag_name):
auth.verify_privilege(ctx.user, 'tags:view')
tag = tags.get_tag_by_name(tag_name)
if not tag:
raise tags.TagNotFoundError('Tag %r not found.' % tag_name)
result = tags.get_siblings(tag)
serialized_siblings = []
for sibling, occurrences in result:
serialized_siblings.append({
'tag': _serialize_tag(sibling),
'occurrences': occurrences
})
return {'siblings': serialized_siblings}

View file

@ -53,7 +53,8 @@ def create_app():
tag_category_detail_api = api.TagCategoryDetailApi()
tag_list_api = api.TagListApi()
tag_detail_api = api.TagDetailApi()
tag_merging_api = api.TagMergingApi()
tag_merge_api = api.TagMergeApi()
tag_siblings_api = api.TagSiblingsApi()
password_reset_api = api.PasswordResetApi()
app.add_error_handler(errors.AuthError, _on_auth_error)
@ -69,7 +70,8 @@ def create_app():
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('/tag-merge/', tag_merging_api)
app.add_route('/tag-merge/', tag_merge_api)
app.add_route('/tag-siblings/{tag_name}', tag_siblings_api)
app.add_route('/password-reset/{user_name}', password_reset_api)
return app

View file

@ -98,6 +98,21 @@ def get_or_create_tags_by_names(names):
new_tags.append(new_tag)
return related_tags, new_tags
def get_siblings(tag):
tag_alias = sqlalchemy.orm.aliased(db.Tag)
pt_alias1 = sqlalchemy.orm.aliased(db.PostTag)
pt_alias2 = sqlalchemy.orm.aliased(db.PostTag)
result = db.session \
.query(tag_alias, sqlalchemy.func.count(tag_alias.tag_id)) \
.join(pt_alias1, pt_alias1.tag_id == tag_alias.tag_id) \
.join(pt_alias2, pt_alias2.post_id == pt_alias1.post_id) \
.filter(pt_alias2.tag_id == tag.tag_id) \
.filter(pt_alias1.tag_id != tag.tag_id) \
.group_by(tag_alias.tag_id) \
.order_by(tag_alias.post_count.desc()) \
.limit(50)
return result
def merge_tags(source_tag, target_tag):
db.session.execute(
sqlalchemy.sql.expression.update(db.PostTag) \

View file

@ -25,7 +25,7 @@ def test_ctx(
ret.context_factory = context_factory
ret.user_factory = user_factory
ret.tag_factory = tag_factory
ret.api = api.TagMergingApi()
ret.api = api.TagMergeApi()
return ret
def test_merging_without_usages(test_ctx, fake_datetime):

View file

@ -0,0 +1,96 @@
import datetime
import pytest
from szurubooru import api, db, errors
from szurubooru.func import util, tags
def assert_results(result, expected_tag_names_and_occurrences):
actual_tag_names_and_occurences = {}
for item in result['siblings']:
tag_name = item['tag']['names'][0]
occurrences = item['occurrences']
actual_tag_names_and_occurences[tag_name] = occurrences
assert actual_tag_names_and_occurences == expected_tag_names_and_occurrences
@pytest.fixture
def test_ctx(
context_factory, config_injector, user_factory, tag_factory, post_factory):
config_injector({
'privileges': {
'tags:view': 'regular_user',
},
'thumbnails': {'avatar_width': 200},
'ranks': ['anonymous', 'regular_user'],
})
ret = util.dotdict()
ret.context_factory = context_factory
ret.user_factory = user_factory
ret.tag_factory = tag_factory
ret.post_factory = post_factory
ret.api = api.TagSiblingsApi()
return ret
def test_unused(test_ctx):
db.session.add(test_ctx.tag_factory(names=['tag']))
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), 'tag')
assert_results(result, {})
def test_used_alone(test_ctx):
tag = test_ctx.tag_factory(names=['tag'])
post = test_ctx.post_factory()
post.tags = [tag]
db.session.add_all([post, tag])
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), 'tag')
assert_results(result, {})
def test_used_with_others(test_ctx):
tag1 = test_ctx.tag_factory(names=['tag1'])
tag2 = test_ctx.tag_factory(names=['tag2'])
post = test_ctx.post_factory()
post.tags = [tag1, tag2]
db.session.add_all([post, tag1, tag2])
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), 'tag1')
assert_results(result, {'tag2': 1})
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), 'tag2')
assert_results(result, {'tag1': 1})
def test_used_with_multiple_others(test_ctx):
tag1 = test_ctx.tag_factory(names=['tag1'])
tag2 = test_ctx.tag_factory(names=['tag2'])
tag3 = test_ctx.tag_factory(names=['tag3'])
post1 = test_ctx.post_factory()
post2 = test_ctx.post_factory()
post1.tags = [tag1, tag2, tag3]
post2.tags = [tag1, tag3]
db.session.add_all([post1, post2, tag1, tag2, tag3])
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), 'tag1')
assert_results(result, {'tag2': 1, 'tag3': 2})
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), 'tag2')
assert_results(result, {'tag1': 1, 'tag3': 1})
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), 'tag3')
assert_results(result, {'tag1': 2, 'tag2': 1})
def test_trying_to_retrieve_non_existing(test_ctx):
with pytest.raises(tags.TagNotFoundError):
test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')), '-')
def test_trying_to_retrieve_without_privileges(test_ctx):
with pytest.raises(errors.AuthError):
test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='anonymous')), '-')