server/snapshots: add snapshot lists

This commit is contained in:
rr- 2016-04-21 19:25:38 +02:00
parent c2a39a0fd5
commit 46ee9faf72
13 changed files with 227 additions and 77 deletions

59
API.md
View file

@ -36,6 +36,8 @@
- Password reset
- [Password reset - step 1: mail request](#password-reset---step-2-confirmation)
- [Password reset - step 2: confirmation](#password-reset---step-2-confirmation)
- Snapshots
- [Listing snapshots](#listing-snapshots)
3. [Resources](#resources)
@ -602,8 +604,6 @@ data.
"pageSize": <page-size>,
"total": <total-count>,
"users": [
<user>,
<user>,
<user>,
<user>,
<user>
@ -861,6 +861,61 @@ data.
it is recommended to connect through HTTPS.
## Listing snapshots
- **Request**
`GET /snapshots/?page=<page>&pageSize=<page-size>&query=<query>`
- **Output**
```json5
{
"query": <query>, // same as in input
"page": <page>, // same as in input
"pageSize": <page-size>,
"total": <total-count>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<snapshot>` is a [snapshot resource](#snapshot) and `query`
contains standard [search query](#search).
- **Errors**
- privileges are too low
- **Description**
Lists recent resource snapshots.
**Anonymous tokens**
Not supported.
**Named tokens**
| `<value>` | Description |
| ----------------- | --------------------------------------------- |
| `type` | involving given resource type |
| `id` | involving given resource id |
| `date` | created at given date |
| `time` | alias of `date` |
| `operation` | `changed`, `created` or `deleted` |
| `user` | name of the user that created given snapshot |
**Order tokens**
None. The snapshots are always sorted by creation time.
**Special tokens**
None.
# Resources

View file

@ -63,47 +63,47 @@ password_regex: '^.{5,}$'
user_name_regex: '^[a-zA-Z0-9_-]{1,32}$'
privileges:
'users:create': anonymous
'users:list': regular_user
'users:view': regular_user
'users:edit:any:name': mod
'users:edit:any:pass': mod
'users:edit:any:email': mod
'users:edit:any:avatar': mod
'users:edit:any:rank': mod
'users:edit:self:name': regular_user
'users:edit:self:pass': regular_user
'users:edit:self:email': regular_user
'users:edit:self:avatar': regular_user
'users:edit:self:rank': mod # one can't promote themselves or anyone to upper rank than their own.
'users:delete:any': admin
'users:delete:self': regular_user
'users:create': anonymous
'users:list': regular_user
'users:view': regular_user
'users:edit:any:name': mod
'users:edit:any:pass': mod
'users:edit:any:email': mod
'users:edit:any:avatar': mod
'users:edit:any:rank': mod
'users:edit:self:name': regular_user
'users:edit:self:pass': regular_user
'users:edit:self:email': regular_user
'users:edit:self:avatar': regular_user
'users:edit:self:rank': mod # one can't promote themselves or anyone to upper rank than their own.
'users:delete:any': admin
'users:delete:self': regular_user
'posts:create:anonymous': regular_user
'posts:create:identified': regular_user
'posts:list': anonymous
'posts:view': anonymous
'posts:edit:content': power_user
'posts:edit:flags': regular_user
'posts:edit:notes': regular_user
'posts:edit:relations': regular_user
'posts:edit:safety': power_user
'posts:edit:source': regular_user
'posts:edit:tags': regular_user
'posts:edit:thumbnail': power_user
'posts:feature': mod
'posts:delete': mod
'posts:create:anonymous': regular_user
'posts:create:identified': regular_user
'posts:list': anonymous
'posts:view': anonymous
'posts:edit:content': power_user
'posts:edit:flags': regular_user
'posts:edit:notes': regular_user
'posts:edit:relations': regular_user
'posts:edit:safety': power_user
'posts:edit:source': regular_user
'posts:edit:tags': regular_user
'posts:edit:thumbnail': power_user
'posts:feature': mod
'posts:delete': mod
'tags:create': regular_user
'tags:edit:names': power_user
'tags:edit:category': power_user
'tags:edit:implications': power_user
'tags:edit:suggestions': power_user
'tags:list': regular_user # note: will be available as data_url/tags.json anyway
'tags:view': anonymous
'tags:masstag': power_user
'tags:merge': mod
'tags:delete': mod
'tags:create': regular_user
'tags:edit:names': power_user
'tags:edit:category': power_user
'tags:edit:implications': power_user
'tags:edit:suggestions': power_user
'tags:list': regular_user # note: will be available as data_url/tags.json anyway
'tags:view': anonymous
'tags:masstag': power_user
'tags:merge': mod
'tags:delete': mod
'tag_categories:create': mod
'tag_categories:edit:name': mod
@ -112,11 +112,11 @@ privileges:
'tag_categories:view': anonymous
'tag_categories:delete': mod
'comments:create': regular_user
'comments:delete:any': mod
'comments:delete:own': regular_user
'comments:edit:any': mod
'comments:edit:own': regular_user
'comments:list': regular_user
'comments:create': regular_user
'comments:delete:any': mod
'comments:delete:own': regular_user
'comments:edit:any': mod
'comments:edit:own': regular_user
'comments:list': regular_user
'history:view': power_user
'snapshots:list': power_user

View file

@ -4,4 +4,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, TagMergeApi, TagSiblingsApi
from szurubooru.api.tag_category_api import TagCategoryListApi, TagCategoryDetailApi
from szurubooru.api.snapshot_api import SnapshotListApi
from szurubooru.api.context import Context, Request

View file

@ -0,0 +1,17 @@
from szurubooru import search
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, snapshots
def _serialize_snapshot(snapshot):
earlier_snapshot = snapshots.get_previous_snapshot(snapshot)
return snapshots.serialize_snapshot(snapshot, earlier_snapshot)
class SnapshotListApi(BaseApi):
def __init__(self):
super().__init__()
self._search_executor = search.SearchExecutor(search.SnapshotSearchConfig())
def get(self, ctx):
auth.verify_privilege(ctx.user, 'snapshots:list')
return self._search_executor.execute_and_serialize(
ctx, _serialize_snapshot, 'snapshots')

View file

@ -28,18 +28,8 @@ class TagListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'tags:list')
query = ctx.get_param_as_string('query')
page = ctx.get_param_as_int('page', default=1, min=1)
page_size = ctx.get_param_as_int(
'pageSize', default=100, min=1, max=100)
count, tag_list = self._search_executor.execute(query, page, page_size)
return {
'query': query,
'page': page,
'pageSize': page_size,
'total': count,
'tags': [_serialize_tag(tag) for tag in tag_list],
}
return self._search_executor.execute_and_serialize(
ctx, _serialize_tag, 'tags')
def post(self, ctx):
auth.verify_privilege(ctx.user, 'tags:create')

View file

@ -35,18 +35,8 @@ class UserListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'users:list')
query = ctx.get_param_as_string('query')
page = ctx.get_param_as_int('page', default=1, min=1)
page_size = ctx.get_param_as_int(
'pageSize', default=100, min=1, max=100)
count, user_list = self._search_executor.execute(query, page, page_size)
return {
'query': query,
'page': page,
'pageSize': page_size,
'total': count,
'users': [_serialize_user(ctx.user, user) for user in user_list],
}
return self._search_executor.execute_and_serialize(
ctx, lambda user: _serialize_user(ctx.user, user), 'users')
def post(self, ctx):
auth.verify_privilege(ctx.user, 'users:create')

View file

@ -56,6 +56,7 @@ def create_app():
tag_merge_api = api.TagMergeApi()
tag_siblings_api = api.TagSiblingsApi()
password_reset_api = api.PasswordResetApi()
snapshot_list_api = api.SnapshotListApi()
app.add_error_handler(errors.AuthError, _on_auth_error)
app.add_error_handler(errors.IntegrityError, _on_integrity_error)
@ -73,5 +74,6 @@ def create_app():
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)
app.add_route('/snapshots/', snapshot_list_api)
return app

View file

@ -43,6 +43,16 @@ def get_resource_info(entity):
return (resource_type, resource_id, resource_repr)
def get_previous_snapshot(snapshot):
return db.session \
.query(db.Snapshot) \
.filter(db.Snapshot.resource_type == snapshot.resource_type) \
.filter(db.Snapshot.resource_id == snapshot.resource_id) \
.filter(db.Snapshot.creation_time < snapshot.creation_time) \
.order_by(db.Snapshot.creation_time.desc()) \
.limit(1) \
.first()
def get_snapshots(entity):
resource_type, resource_id, _ = get_resource_info(entity)
return db.session \
@ -57,7 +67,7 @@ def serialize_snapshot(snapshot, earlier_snapshot):
'operation': snapshot.operation,
'type': snapshot.resource_type,
'id': snapshot.resource_repr,
'user': snapshot.user.name,
'user': snapshot.user.name if snapshot.user else None,
'data': snapshot.data,
'earlier-data': earlier_snapshot.data if earlier_snapshot else None,
'time': snapshot.creation_time,

View file

@ -2,4 +2,5 @@
from szurubooru.search.search_executor import SearchExecutor
from szurubooru.search.user_search_config import UserSearchConfig
from szurubooru.search.snapshot_search_config import SnapshotSearchConfig
from szurubooru.search.tag_search_config import TagSearchConfig

View file

@ -12,19 +12,19 @@ class BaseSearchConfig(object):
@property
def anonymous_filter(self):
raise NotImplementedError()
return None
@property
def special_filters(self):
raise NotImplementedError()
return {}
@property
def named_filters(self):
raise NotImplementedError()
return {}
@property
def order_columns(self):
raise NotImplementedError()
return {}
@staticmethod
def _apply_num_criterion_to_column(column, criterion):

View file

@ -28,6 +28,19 @@ class SearchExecutor(object):
.scalar()
return (count, entities)
def execute_and_serialize(self, ctx, serializer, key_name):
query = ctx.get_param_as_string('query')
page = ctx.get_param_as_int('page', default=1, min=1)
page_size = ctx.get_param_as_int('pageSize', default=100, min=1, max=100)
count, entities = self.execute(query, page, page_size)
return {
'query': query,
'page': page,
'pageSize': page_size,
'total': count,
key_name: [serializer(entity) for entity in entities],
}
def _prepare(self, query_text):
''' Parse input and return SQLAlchemy query. '''
query = self._search_config.create_query() \

View file

@ -0,0 +1,20 @@
from szurubooru import db
from szurubooru.search.base_search_config import BaseSearchConfig
class SnapshotSearchConfig(BaseSearchConfig):
def create_query(self):
return db.session.query(db.Snapshot)
def finalize_query(self, query):
return query.order_by(db.Snapshot.creation_time.desc())
@property
def named_filters(self):
return {
'type': self._create_str_filter(db.Snapshot.resource_type),
'id': self._create_str_filter(db.Snapshot.resource_repr),
'date': self._create_date_filter(db.Snapshot.creation_time),
'time': self._create_str_filter(db.Snapshot.creation_time),
'operation': self._create_str_filter(db.Snapshot.operation),
'user': self._create_str_filter(db.User.name),
}

View file

@ -0,0 +1,51 @@
import datetime
import pytest
from szurubooru import api, db, errors
from szurubooru.func import util, tags
def snapshot_factory():
snapshot = db.Snapshot()
snapshot.creation_time = datetime.datetime(1999, 1, 1)
snapshot.resource_type = 'dummy'
snapshot.resource_id = 1
snapshot.resource_repr = 'dummy'
snapshot.operation = 'added'
snapshot.data = '{}'
return snapshot
@pytest.fixture
def test_ctx(context_factory, config_injector, user_factory):
config_injector({
'privileges': {
'snapshots:list': 'regular_user',
},
'thumbnails': {'avatar_width': 200},
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
'rank_names': {'regular_user': 'Peasant'},
})
ret = util.dotdict()
ret.context_factory = context_factory
ret.user_factory = user_factory
ret.api = api.SnapshotListApi()
return ret
def test_retrieving_multiple(test_ctx):
snapshot1 = snapshot_factory()
snapshot2 = snapshot_factory()
db.session.add_all([snapshot1, snapshot2])
result = test_ctx.api.get(
test_ctx.context_factory(
input={'query': '', 'page': 1},
user=test_ctx.user_factory(rank='regular_user')))
assert result['query'] == ''
assert result['page'] == 1
assert result['pageSize'] == 100
assert result['total'] == 2
assert len(result['snapshots']) == 2
def test_trying_to_retrieve_multiple_without_privileges(test_ctx):
with pytest.raises(errors.AuthError):
test_ctx.api.get(
test_ctx.context_factory(
input={'query': '', 'page': 1},
user=test_ctx.user_factory(rank='anonymous')))