server/posts: add featured post retrieval
This commit is contained in:
parent
cf00a3a2de
commit
1476c84a9d
57
API.md
57
API.md
|
@ -36,6 +36,7 @@
|
||||||
- ~~Scoring posts~~
|
- ~~Scoring posts~~
|
||||||
- ~~Adding posts to favorites~~
|
- ~~Adding posts to favorites~~
|
||||||
- ~~Removing posts from favorites~~
|
- ~~Removing posts from favorites~~
|
||||||
|
- [Getting featured post](#getting-featured-post)
|
||||||
- [Featuring post](#featuring-post)
|
- [Featuring post](#featuring-post)
|
||||||
- Users
|
- Users
|
||||||
- [Listing users](#listing-users)
|
- [Listing users](#listing-users)
|
||||||
|
@ -48,6 +49,8 @@
|
||||||
- [Password reset - step 2: confirmation](#password-reset---step-2-confirmation)
|
- [Password reset - step 2: confirmation](#password-reset---step-2-confirmation)
|
||||||
- Snapshots
|
- Snapshots
|
||||||
- [Listing snapshots](#listing-snapshots)
|
- [Listing snapshots](#listing-snapshots)
|
||||||
|
- Global info
|
||||||
|
- [Getting global info](#getting-global-info)
|
||||||
|
|
||||||
3. [Resources](#resources)
|
3. [Resources](#resources)
|
||||||
|
|
||||||
|
@ -601,6 +604,37 @@ data.
|
||||||
list is truncated to the first 50 elements. Doesn't use paging.
|
list is truncated to the first 50 elements. Doesn't use paging.
|
||||||
|
|
||||||
|
|
||||||
|
## Getting featured post
|
||||||
|
- **Request**
|
||||||
|
|
||||||
|
`GET /featured-post`
|
||||||
|
|
||||||
|
- **Output**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"post": <post>,
|
||||||
|
"snapshots": [
|
||||||
|
<snapshot>,
|
||||||
|
<snapshot>,
|
||||||
|
<snapshot>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
...where `<post>` is a [post resource](#post), and `snapshots` contain its
|
||||||
|
earlier versions.
|
||||||
|
|
||||||
|
- **Errors**
|
||||||
|
|
||||||
|
- privileges are too low
|
||||||
|
|
||||||
|
- **Description**
|
||||||
|
|
||||||
|
Retrieves the post that is currently featured on the main page in web
|
||||||
|
client. If no post is featured, `<post>` is null and `snapshots` array is
|
||||||
|
empty.
|
||||||
|
|
||||||
|
|
||||||
## Featuring post
|
## Featuring post
|
||||||
- **Request**
|
- **Request**
|
||||||
|
|
||||||
|
@ -628,7 +662,7 @@ data.
|
||||||
|
|
||||||
- **Description**
|
- **Description**
|
||||||
|
|
||||||
Features a post on the main page.
|
Features a post on the main page in web client.
|
||||||
|
|
||||||
|
|
||||||
## Listing users
|
## Listing users
|
||||||
|
@ -957,6 +991,27 @@ data.
|
||||||
None.
|
None.
|
||||||
|
|
||||||
|
|
||||||
|
## Getting global info
|
||||||
|
- **Request**
|
||||||
|
|
||||||
|
`GET /info`
|
||||||
|
|
||||||
|
- **Output**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"postCount": <post-count>,
|
||||||
|
"diskUsage": <disk-usage>, // in bytes
|
||||||
|
"featuredPost": <featured-post>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description**
|
||||||
|
|
||||||
|
Retrieves simple statistics. `<featured-post>` is null if there is no
|
||||||
|
featured post yet.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Resources
|
# Resources
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from szurubooru import config
|
from szurubooru import config
|
||||||
|
from szurubooru.api.post_api import serialize_post
|
||||||
from szurubooru.api.base_api import BaseApi
|
from szurubooru.api.base_api import BaseApi
|
||||||
from szurubooru.func import posts
|
from szurubooru.func import posts
|
||||||
|
|
||||||
|
@ -10,10 +11,12 @@ class InfoApi(BaseApi):
|
||||||
self._cache_time = None
|
self._cache_time = None
|
||||||
self._cache_result = None
|
self._cache_result = None
|
||||||
|
|
||||||
def get(self, _ctx):
|
def get(self, ctx):
|
||||||
|
featured_post = posts.get_featured_post()
|
||||||
return {
|
return {
|
||||||
'postCount': posts.get_post_count(),
|
'postCount': posts.get_post_count(),
|
||||||
'diskUsage': self._get_disk_usage()
|
'diskUsage': self._get_disk_usage(),
|
||||||
|
'featuredPost': serialize_post(featured_post, ctx.user),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_disk_usage(self):
|
def _get_disk_usage(self):
|
||||||
|
|
|
@ -3,6 +3,9 @@ from szurubooru.api.user_api import serialize_user
|
||||||
from szurubooru.func import auth, posts, snapshots
|
from szurubooru.func import auth, posts, snapshots
|
||||||
|
|
||||||
def serialize_post(post, authenticated_user):
|
def serialize_post(post, authenticated_user):
|
||||||
|
if not post:
|
||||||
|
return None
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
'id': post.post_id,
|
'id': post.post_id,
|
||||||
'creationTime': post.creation_time,
|
'creationTime': post.creation_time,
|
||||||
|
@ -56,3 +59,7 @@ class PostFeatureApi(BaseApi):
|
||||||
snapshots.modify(post, ctx.user)
|
snapshots.modify(post, ctx.user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
return serialize_post_with_details(post, ctx.user)
|
return serialize_post_with_details(post, ctx.user)
|
||||||
|
|
||||||
|
def get(self, ctx):
|
||||||
|
post = posts.get_featured_post()
|
||||||
|
return serialize_post_with_details(post, ctx.user)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from szurubooru import search
|
||||||
from szurubooru.api.base_api import BaseApi
|
from szurubooru.api.base_api import BaseApi
|
||||||
from szurubooru.func import auth, snapshots
|
from szurubooru.func import auth, snapshots
|
||||||
|
|
||||||
def _serialize_snapshot(snapshot):
|
def serialize_snapshot(snapshot):
|
||||||
earlier_snapshot = snapshots.get_previous_snapshot(snapshot)
|
earlier_snapshot = snapshots.get_previous_snapshot(snapshot)
|
||||||
return snapshots.serialize_snapshot(snapshot, earlier_snapshot)
|
return snapshots.serialize_snapshot(snapshot, earlier_snapshot)
|
||||||
|
|
||||||
|
@ -14,4 +14,4 @@ class SnapshotListApi(BaseApi):
|
||||||
def get(self, ctx):
|
def get(self, ctx):
|
||||||
auth.verify_privilege(ctx.user, 'snapshots:list')
|
auth.verify_privilege(ctx.user, 'snapshots:list')
|
||||||
return self._search_executor.execute_and_serialize(
|
return self._search_executor.execute_and_serialize(
|
||||||
ctx, _serialize_snapshot, 'snapshots')
|
ctx, serialize_snapshot, 'snapshots')
|
||||||
|
|
|
@ -3,7 +3,7 @@ from szurubooru import search
|
||||||
from szurubooru.api.base_api import BaseApi
|
from szurubooru.api.base_api import BaseApi
|
||||||
from szurubooru.func import auth, tags, snapshots
|
from szurubooru.func import auth, tags, snapshots
|
||||||
|
|
||||||
def _serialize_tag(tag):
|
def serialize_tag(tag):
|
||||||
return {
|
return {
|
||||||
'names': [tag_name.name for tag_name in tag.names],
|
'names': [tag_name.name for tag_name in tag.names],
|
||||||
'category': tag.category.name,
|
'category': tag.category.name,
|
||||||
|
@ -15,9 +15,9 @@ def _serialize_tag(tag):
|
||||||
'lastEditTime': tag.last_edit_time,
|
'lastEditTime': tag.last_edit_time,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_tag_with_details(tag):
|
def serialize_tag_with_details(tag):
|
||||||
return {
|
return {
|
||||||
'tag': _serialize_tag(tag),
|
'tag': serialize_tag(tag),
|
||||||
'snapshots': snapshots.get_serialized_history(tag),
|
'snapshots': snapshots.get_serialized_history(tag),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class TagListApi(BaseApi):
|
||||||
def get(self, ctx):
|
def get(self, ctx):
|
||||||
auth.verify_privilege(ctx.user, 'tags:list')
|
auth.verify_privilege(ctx.user, 'tags:list')
|
||||||
return self._search_executor.execute_and_serialize(
|
return self._search_executor.execute_and_serialize(
|
||||||
ctx, _serialize_tag, 'tags')
|
ctx, serialize_tag, 'tags')
|
||||||
|
|
||||||
def post(self, ctx):
|
def post(self, ctx):
|
||||||
auth.verify_privilege(ctx.user, 'tags:create')
|
auth.verify_privilege(ctx.user, 'tags:create')
|
||||||
|
@ -47,7 +47,7 @@ class TagListApi(BaseApi):
|
||||||
snapshots.create(tag, ctx.user)
|
snapshots.create(tag, ctx.user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
tags.export_to_json()
|
tags.export_to_json()
|
||||||
return _serialize_tag_with_details(tag)
|
return serialize_tag_with_details(tag)
|
||||||
|
|
||||||
class TagDetailApi(BaseApi):
|
class TagDetailApi(BaseApi):
|
||||||
def get(self, ctx, tag_name):
|
def get(self, ctx, tag_name):
|
||||||
|
@ -55,7 +55,7 @@ class TagDetailApi(BaseApi):
|
||||||
tag = tags.get_tag_by_name(tag_name)
|
tag = tags.get_tag_by_name(tag_name)
|
||||||
if not tag:
|
if not tag:
|
||||||
raise tags.TagNotFoundError('Tag %r not found.' % tag_name)
|
raise tags.TagNotFoundError('Tag %r not found.' % tag_name)
|
||||||
return _serialize_tag_with_details(tag)
|
return serialize_tag_with_details(tag)
|
||||||
|
|
||||||
def put(self, ctx, tag_name):
|
def put(self, ctx, tag_name):
|
||||||
tag = tags.get_tag_by_name(tag_name)
|
tag = tags.get_tag_by_name(tag_name)
|
||||||
|
@ -83,7 +83,7 @@ class TagDetailApi(BaseApi):
|
||||||
snapshots.modify(tag, ctx.user)
|
snapshots.modify(tag, ctx.user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
tags.export_to_json()
|
tags.export_to_json()
|
||||||
return _serialize_tag_with_details(tag)
|
return serialize_tag_with_details(tag)
|
||||||
|
|
||||||
def delete(self, ctx, tag_name):
|
def delete(self, ctx, tag_name):
|
||||||
tag = tags.get_tag_by_name(tag_name)
|
tag = tags.get_tag_by_name(tag_name)
|
||||||
|
@ -121,7 +121,7 @@ class TagMergeApi(BaseApi):
|
||||||
tags.merge_tags(source_tag, target_tag)
|
tags.merge_tags(source_tag, target_tag)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
tags.export_to_json()
|
tags.export_to_json()
|
||||||
return _serialize_tag_with_details(target_tag)
|
return serialize_tag_with_details(target_tag)
|
||||||
|
|
||||||
class TagSiblingsApi(BaseApi):
|
class TagSiblingsApi(BaseApi):
|
||||||
def get(self, ctx, tag_name):
|
def get(self, ctx, tag_name):
|
||||||
|
@ -133,7 +133,7 @@ class TagSiblingsApi(BaseApi):
|
||||||
serialized_siblings = []
|
serialized_siblings = []
|
||||||
for sibling, occurrences in result:
|
for sibling, occurrences in result:
|
||||||
serialized_siblings.append({
|
serialized_siblings.append({
|
||||||
'tag': _serialize_tag(sibling),
|
'tag': serialize_tag(sibling),
|
||||||
'occurrences': occurrences
|
'occurrences': occurrences
|
||||||
})
|
})
|
||||||
return {'siblings': serialized_siblings}
|
return {'siblings': serialized_siblings}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from szurubooru.api.base_api import BaseApi
|
from szurubooru.api.base_api import BaseApi
|
||||||
from szurubooru.func import auth, tags, tag_categories, snapshots
|
from szurubooru.func import auth, tags, tag_categories, snapshots
|
||||||
|
|
||||||
def _serialize_category(category):
|
def serialize_category(category):
|
||||||
return {
|
return {
|
||||||
'name': category.name,
|
'name': category.name,
|
||||||
'color': category.color,
|
'color': category.color,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_category_with_details(category):
|
def serialize_category_with_details(category):
|
||||||
return {
|
return {
|
||||||
'tagCategory': _serialize_category(category),
|
'tagCategory': serialize_category(category),
|
||||||
'snapshots': snapshots.get_serialized_history(category),
|
'snapshots': snapshots.get_serialized_history(category),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class TagCategoryListApi(BaseApi):
|
||||||
categories = tag_categories.get_all_categories()
|
categories = tag_categories.get_all_categories()
|
||||||
return {
|
return {
|
||||||
'tagCategories': [
|
'tagCategories': [
|
||||||
_serialize_category(category) for category in categories],
|
serialize_category(category) for category in categories],
|
||||||
}
|
}
|
||||||
|
|
||||||
def post(self, ctx):
|
def post(self, ctx):
|
||||||
|
@ -32,7 +32,7 @@ class TagCategoryListApi(BaseApi):
|
||||||
snapshots.create(category, ctx.user)
|
snapshots.create(category, ctx.user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
tags.export_to_json()
|
tags.export_to_json()
|
||||||
return _serialize_category_with_details(category)
|
return serialize_category_with_details(category)
|
||||||
|
|
||||||
class TagCategoryDetailApi(BaseApi):
|
class TagCategoryDetailApi(BaseApi):
|
||||||
def get(self, ctx, category_name):
|
def get(self, ctx, category_name):
|
||||||
|
@ -41,7 +41,7 @@ class TagCategoryDetailApi(BaseApi):
|
||||||
if not category:
|
if not category:
|
||||||
raise tag_categories.TagCategoryNotFoundError(
|
raise tag_categories.TagCategoryNotFoundError(
|
||||||
'Tag category %r not found.' % category_name)
|
'Tag category %r not found.' % category_name)
|
||||||
return _serialize_category_with_details(category)
|
return serialize_category_with_details(category)
|
||||||
|
|
||||||
def put(self, ctx, category_name):
|
def put(self, ctx, category_name):
|
||||||
category = tag_categories.get_category_by_name(category_name)
|
category = tag_categories.get_category_by_name(category_name)
|
||||||
|
@ -60,7 +60,7 @@ class TagCategoryDetailApi(BaseApi):
|
||||||
snapshots.modify(category, ctx.user)
|
snapshots.modify(category, ctx.user)
|
||||||
ctx.session.commit()
|
ctx.session.commit()
|
||||||
tags.export_to_json()
|
tags.export_to_json()
|
||||||
return _serialize_category_with_details(category)
|
return serialize_category_with_details(category)
|
||||||
|
|
||||||
def delete(self, ctx, category_name):
|
def delete(self, ctx, category_name):
|
||||||
category = tag_categories.get_category_by_name(category_name)
|
category = tag_categories.get_category_by_name(category_name)
|
||||||
|
|
|
@ -91,6 +91,8 @@ def serialize_snapshot(snapshot, earlier_snapshot):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_serialized_history(entity):
|
def get_serialized_history(entity):
|
||||||
|
if not entity:
|
||||||
|
return []
|
||||||
ret = []
|
ret = []
|
||||||
earlier_snapshot = None
|
earlier_snapshot = None
|
||||||
for snapshot in reversed(get_snapshots(entity)):
|
for snapshot in reversed(get_snapshots(entity)):
|
||||||
|
|
|
@ -11,15 +11,18 @@ def test_info_api(
|
||||||
assert info_api.get(context_factory()) == {
|
assert info_api.get(context_factory()) == {
|
||||||
'postCount': 2,
|
'postCount': 2,
|
||||||
'diskUsage': 3,
|
'diskUsage': 3,
|
||||||
|
'featuredPost': None,
|
||||||
}
|
}
|
||||||
directory.join('test2.txt').write('abc')
|
directory.join('test2.txt').write('abc')
|
||||||
with fake_datetime('13:59'):
|
with fake_datetime('13:59'):
|
||||||
assert info_api.get(context_factory()) == {
|
assert info_api.get(context_factory()) == {
|
||||||
'postCount': 2,
|
'postCount': 2,
|
||||||
'diskUsage': 3, # still 3 - it's cached
|
'diskUsage': 3, # still 3 - it's cached
|
||||||
|
'featuredPost': None,
|
||||||
}
|
}
|
||||||
with fake_datetime('14:01'):
|
with fake_datetime('14:01'):
|
||||||
assert info_api.get(context_factory()) == {
|
assert info_api.get(context_factory()) == {
|
||||||
'postCount': 2,
|
'postCount': 2,
|
||||||
'diskUsage': 6, # cache expired
|
'diskUsage': 6, # cache expired
|
||||||
|
'featuredPost': None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,10 @@ from szurubooru.func import util, posts
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_ctx(context_factory, config_injector, user_factory, post_factory):
|
def test_ctx(context_factory, config_injector, user_factory, post_factory):
|
||||||
config_injector({
|
config_injector({
|
||||||
'privileges': {'posts:feature': 'regular_user'},
|
'privileges': {
|
||||||
|
'posts:feature': 'regular_user',
|
||||||
|
'posts:view': 'regular_user',
|
||||||
|
},
|
||||||
'ranks': ['anonymous', 'regular_user'],
|
'ranks': ['anonymous', 'regular_user'],
|
||||||
})
|
})
|
||||||
ret = util.dotdict()
|
ret = util.dotdict()
|
||||||
|
@ -16,10 +19,16 @@ def test_ctx(context_factory, config_injector, user_factory, post_factory):
|
||||||
ret.api = api.PostFeatureApi()
|
ret.api = api.PostFeatureApi()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def test_no_featured_post(test_ctx):
|
||||||
|
assert posts.get_featured_post() is None
|
||||||
|
result = test_ctx.api.get(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
assert result == {'post': None, 'snapshots': []}
|
||||||
|
|
||||||
def test_featuring(test_ctx):
|
def test_featuring(test_ctx):
|
||||||
db.session.add(test_ctx.post_factory(id=1))
|
db.session.add(test_ctx.post_factory(id=1))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
assert posts.get_featured_post() is None
|
|
||||||
assert not posts.get_post_by_id(1).is_featured
|
assert not posts.get_post_by_id(1).is_featured
|
||||||
result = test_ctx.api.post(
|
result = test_ctx.api.post(
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
|
@ -31,6 +40,11 @@ def test_featuring(test_ctx):
|
||||||
assert 'post' in result
|
assert 'post' in result
|
||||||
assert 'snapshots' in result
|
assert 'snapshots' in result
|
||||||
assert 'id' in result['post']
|
assert 'id' in result['post']
|
||||||
|
result = test_ctx.api.get(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
assert 'post' in result
|
||||||
|
assert 'id' in result['post']
|
||||||
|
|
||||||
def test_trying_to_feature_the_same_post_twice(test_ctx):
|
def test_trying_to_feature_the_same_post_twice(test_ctx):
|
||||||
db.session.add(test_ctx.post_factory(id=1))
|
db.session.add(test_ctx.post_factory(id=1))
|
||||||
|
@ -80,3 +94,11 @@ def test_trying_to_feature_without_privileges(test_ctx):
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
input={'id': 1},
|
input={'id': 1},
|
||||||
user=test_ctx.user_factory(rank='anonymous')))
|
user=test_ctx.user_factory(rank='anonymous')))
|
||||||
|
|
||||||
|
def test_getting_featured_post_without_privileges_to_view(test_ctx):
|
||||||
|
try:
|
||||||
|
test_ctx.api.get(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
user=test_ctx.user_factory(rank='anonymous')))
|
||||||
|
except:
|
||||||
|
pytest.fail()
|
||||||
|
|
Loading…
Reference in New Issue