server/tags: add listing tag siblings
This commit is contained in:
parent
747c730688
commit
97bd935bb6
7 changed files with 170 additions and 6 deletions
38
API.md
38
API.md
|
@ -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**
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) \
|
||||
|
|
|
@ -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):
|
||||
|
|
96
server/szurubooru/tests/api/test_tag_siblings_retrieving.py
Normal file
96
server/szurubooru/tests/api/test_tag_siblings_retrieving.py
Normal 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')), '-')
|
Loading…
Reference in a new issue