diff --git a/API.md b/API.md index 9400c6f..3c51c4f 100644 --- a/API.md +++ b/API.md @@ -142,10 +142,16 @@ data. ```json5 { - "tagCategory": + "tagCategory": , + "snapshots": [ + {"data": , "time": }, + {"data": , "time": }, + {"data": , "time": } + ] } ``` - ...where `` is a [tag category resource](#tag-category). + ...where `` is a [tag category resource](#tag-category), and + `snapshots` contain its earlier versions. - **Errors** @@ -178,10 +184,16 @@ data. ```json5 { - "tagCategory": + "tagCategory": , + "snapshots": [ + {"data": , "time": }, + {"data": , "time": }, + {"data": , "time": } + ] } ``` - ...where `` is a [tag category resource](#tag-category). + ...where `` is a [tag category resource](#tag-category), and + `snapshots` contain its earlier versions. - **Errors** @@ -207,10 +219,16 @@ data. ```json5 { - "tagCategory": + "tagCategory": , + "snapshots": [ + {"data": , "time": }, + {"data": , "time": }, + {"data": , "time": } + ] } ``` - ...where `` is a [tag category resource](#tag-category). + ...where `` is a [tag category resource](#tag-category), and + `snapshots` contain its earlier versions. - **Errors** @@ -351,10 +369,16 @@ data. ```json5 { - "tag": + "tag": , + "snapshots": [ + {"data": , "time": }, + {"data": , "time": }, + {"data": , "time": } + ] } ``` - ...where `` is a [tag resource](#tag). + ...where `` is a [tag resource](#tag), and `snapshots` contain its + earlier versions. - **Errors** @@ -399,10 +423,16 @@ data. ```json5 { - "tag": + "tag": , + "snapshots": [ + {"data": , "time": }, + {"data": , "time": }, + {"data": , "time": } + ] } ``` - ...where `` is a [tag resource](#tag). + ...where `` is a [tag resource](#tag), and `snapshots` contain its + earlier versions. - **Errors** @@ -435,10 +465,16 @@ data. ```json5 { - "tag": + "tag": , + "snapshots": [ + {"data": , "time": }, + {"data": , "time": }, + {"data": , "time": } + ] } ``` - ...where `` is a [tag resource](#tag). + ...where `` is a [tag resource](#tag), and `snapshots` contain its + earlier versions. - **Errors** @@ -768,7 +804,16 @@ data. ```json5 { "name": "character", - "color": "#FF0000", // used to colorize certain tag types in the web client + "color": "#FF0000" // used to colorize certain tag types in the web client +} +``` + +## Tag category snapshot + +```json5 +{ + "name": "character", + "color": "#FF0000" } ``` @@ -777,7 +822,7 @@ data. ```json5 { "names": ["tag1", "tag2", "tag3"], - "category": "plain", // one of values controlled by server's configuration + "category": "plain", "implications": ["implied-tag1", "implied-tag2", "implied-tag3"], "suggestions": ["suggested-tag1", "suggested-tag2", "suggested-tag3"], "creationTime": "2016-03-28T13:37:01.755461", @@ -785,6 +830,16 @@ data. } ``` +## Tag snapshot + +```json5 +{ + "names": ["tag1", "tag2", "tag3"], + "category": "plain", + "implications": ["imp1", "imp2", "imp3"], + "suggestions": ["sug1", "sug2", "sug3"] +} +``` # Search diff --git a/server/szurubooru/api/tag_api.py b/server/szurubooru/api/tag_api.py index 6fcf170..6e6dc9d 100644 --- a/server/szurubooru/api/tag_api.py +++ b/server/szurubooru/api/tag_api.py @@ -15,6 +15,12 @@ def _serialize_tag(tag): 'lastEditTime': tag.last_edit_time, } +def _serialize_tag_with_details(tag): + return { + 'tag': _serialize_tag(tag), + 'snapshots': snapshots.get_data(tag), + } + class TagListApi(BaseApi): def __init__(self): super().__init__() @@ -51,7 +57,7 @@ class TagListApi(BaseApi): snapshots.create(tag, ctx.user) ctx.session.commit() tags.export_to_json() - return {'tag': _serialize_tag(tag)} + return _serialize_tag_with_details(tag) class TagDetailApi(BaseApi): def get(self, ctx, tag_name): @@ -59,7 +65,7 @@ class TagDetailApi(BaseApi): tag = tags.get_tag_by_name(tag_name) if not tag: raise tags.TagNotFoundError('Tag %r not found.' % tag_name) - return {'tag': _serialize_tag(tag)} + return _serialize_tag_with_details(tag) def put(self, ctx, tag_name): tag = tags.get_tag_by_name(tag_name) @@ -86,7 +92,7 @@ class TagDetailApi(BaseApi): snapshots.modify(tag, ctx.user) ctx.session.commit() tags.export_to_json() - return {'tag': _serialize_tag(tag)} + return _serialize_tag_with_details(tag) def delete(self, ctx, tag_name): tag = tags.get_tag_by_name(tag_name) diff --git a/server/szurubooru/api/tag_category_api.py b/server/szurubooru/api/tag_category_api.py index fe8c34a..b60f554 100644 --- a/server/szurubooru/api/tag_category_api.py +++ b/server/szurubooru/api/tag_category_api.py @@ -7,6 +7,12 @@ def _serialize_category(category): 'color': category.color, } +def _serialize_category_with_details(category): + return { + 'tagCategory': _serialize_category(category), + 'snapshots': snapshots.get_data(category), + } + class TagCategoryListApi(BaseApi): def get(self, ctx): auth.verify_privilege(ctx.user, 'tag_categories:list') @@ -26,7 +32,7 @@ class TagCategoryListApi(BaseApi): snapshots.create(category, ctx.user) ctx.session.commit() tags.export_to_json() - return {'tagCategory': _serialize_category(category)} + return _serialize_category_with_details(category) class TagCategoryDetailApi(BaseApi): def get(self, ctx, category_name): @@ -35,7 +41,7 @@ class TagCategoryDetailApi(BaseApi): if not category: raise tag_categories.TagCategoryNotFoundError( 'Tag category %r not found.' % category_name) - return {'tagCategory': _serialize_category(category)} + return _serialize_category_with_details(category) def put(self, ctx, category_name): category = tag_categories.get_category_by_name(category_name) @@ -53,7 +59,7 @@ class TagCategoryDetailApi(BaseApi): snapshots.modify(category, ctx.user) ctx.session.commit() tags.export_to_json() - return {'tagCategory': _serialize_category(category)} + return _serialize_category_with_details(category) def delete(self, ctx, category_name): category = tag_categories.get_category_by_name(category_name) diff --git a/server/szurubooru/tests/api/test_tag_category_creating.py b/server/szurubooru/tests/api/test_tag_category_creating.py index cdb62a8..065e00d 100644 --- a/server/szurubooru/tests/api/test_tag_category_creating.py +++ b/server/szurubooru/tests/api/test_tag_category_creating.py @@ -22,9 +22,8 @@ def test_creating_category(test_ctx): test_ctx.context_factory( input={'name': 'meta', 'color': 'black'}, user=test_ctx.user_factory(rank='regular_user'))) - assert result == { - 'tagCategory': {'name': 'meta', 'color': 'black'}, - } + assert result['tagCategory'] == {'name': 'meta', 'color': 'black'} + assert len(result['snapshots']) == 1 category = db.session.query(db.TagCategory).one() assert category.name == 'meta' assert category.color == 'black' diff --git a/server/szurubooru/tests/api/test_tag_category_retrieving.py b/server/szurubooru/tests/api/test_tag_category_retrieving.py index c581b7f..c9a7ac7 100644 --- a/server/szurubooru/tests/api/test_tag_category_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_category_retrieving.py @@ -43,7 +43,8 @@ def test_retrieving_single(test_ctx): 'tagCategory': { 'name': 'cat', 'color': 'dummy', - } + }, + 'snapshots': [], } def test_trying_to_retrieve_single_non_existing(test_ctx): diff --git a/server/szurubooru/tests/api/test_tag_category_updating.py b/server/szurubooru/tests/api/test_tag_category_updating.py index be576d0..104659c 100644 --- a/server/szurubooru/tests/api/test_tag_category_updating.py +++ b/server/szurubooru/tests/api/test_tag_category_updating.py @@ -38,12 +38,11 @@ def test_simple_updating(test_ctx): }, user=test_ctx.user_factory(rank='regular_user')), 'name') - assert result == { - 'tagCategory': { - 'name': 'changed', - 'color': 'white', - } + assert result['tagCategory'] == { + 'name': 'changed', + 'color': 'white', } + assert len(result['snapshots']) == 1 assert tag_categories.get_category_by_name('name') is None category = tag_categories.get_category_by_name('changed') assert category is not None diff --git a/server/szurubooru/tests/api/test_tag_creating.py b/server/szurubooru/tests/api/test_tag_creating.py index c9ffe9a..e765cc1 100644 --- a/server/szurubooru/tests/api/test_tag_creating.py +++ b/server/szurubooru/tests/api/test_tag_creating.py @@ -45,16 +45,15 @@ def test_creating_simple_tags(test_ctx, fake_datetime): 'implications': [], }, user=test_ctx.user_factory(rank='regular_user'))) - assert result == { - 'tag': { - 'names': ['tag1', 'tag2'], - 'category': 'meta', - 'suggestions': [], - 'implications': [], - 'creationTime': datetime.datetime(1997, 12, 1), - 'lastEditTime': None, - } + assert result['tag'] == { + 'names': ['tag1', 'tag2'], + 'category': 'meta', + 'suggestions': [], + 'implications': [], + 'creationTime': datetime.datetime(1997, 12, 1), + 'lastEditTime': None, } + assert len(result['snapshots']) == 1 tag = get_tag('tag1') assert [tag_name.name for tag_name in tag.names] == ['tag1', 'tag2'] assert tag.category.name == 'meta' diff --git a/server/szurubooru/tests/api/test_tag_retrieving.py b/server/szurubooru/tests/api/test_tag_retrieving.py index a401c9a..c794e6f 100644 --- a/server/szurubooru/tests/api/test_tag_retrieving.py +++ b/server/szurubooru/tests/api/test_tag_retrieving.py @@ -58,7 +58,8 @@ def test_retrieving_single(test_ctx): 'lastEditTime': None, 'suggestions': [], 'implications': [], - } + }, + 'snapshots': [], } def test_trying_to_retrieve_single_non_existing(test_ctx): diff --git a/server/szurubooru/tests/api/test_tag_updating.py b/server/szurubooru/tests/api/test_tag_updating.py index 381b6ae..9ffe21a 100644 --- a/server/szurubooru/tests/api/test_tag_updating.py +++ b/server/szurubooru/tests/api/test_tag_updating.py @@ -52,16 +52,15 @@ def test_simple_updating(test_ctx, fake_datetime): }, user=test_ctx.user_factory(rank='regular_user')), 'tag1') - assert result == { - 'tag': { - 'names': ['tag3'], - 'category': 'character', - 'suggestions': [], - 'implications': [], - 'creationTime': datetime.datetime(1996, 1, 1), - 'lastEditTime': datetime.datetime(1997, 12, 1), - } + assert result['tag'] == { + 'names': ['tag3'], + 'category': 'character', + 'suggestions': [], + 'implications': [], + 'creationTime': datetime.datetime(1996, 1, 1), + 'lastEditTime': datetime.datetime(1997, 12, 1), } + assert len(result['snapshots']) == 1 assert get_tag('tag1') is None assert get_tag('tag2') is None tag = get_tag('tag3') diff --git a/server/szurubooru/tests/util/test_snapshots.py b/server/szurubooru/tests/util/test_snapshots.py index 526d4d0..8057768 100644 --- a/server/szurubooru/tests/util/test_snapshots.py +++ b/server/szurubooru/tests/util/test_snapshots.py @@ -7,7 +7,9 @@ def test_serializing_tag(tag_factory): tag = tag_factory(names=['main_name', 'alias'], category_name='dummy') assert snapshots.get_tag_snapshot(tag) == { 'names': ['main_name', 'alias'], - 'category': 'dummy' + 'category': 'dummy', + 'suggestions': [], + 'implications': [], } tag = tag_factory(names=['main_name', 'alias'], category_name='dummy') @@ -39,10 +41,8 @@ def test_merging_modification_to_creation(tag_factory, user_factory): db.session.add_all([tag, user]) db.session.flush() snapshots.create(tag, user) - db.session.flush() tag.names = [db.TagName('changed')] snapshots.modify(tag, user) - db.session.flush() results = db.session.query(db.Snapshot).all() assert len(results) == 1 assert results[0].operation == db.Snapshot.OPERATION_CREATED @@ -55,15 +55,12 @@ def test_merging_modifications(fake_datetime, tag_factory, user_factory): db.session.flush() with fake_datetime('13:00:00'): snapshots.create(tag, user) - db.session.flush() tag.names = [db.TagName('changed')] with fake_datetime('14:00:00'): snapshots.modify(tag, user) - db.session.flush() tag.names = [db.TagName('changed again')] with fake_datetime('14:00:01'): snapshots.modify(tag, user) - db.session.flush() results = db.session.query(db.Snapshot).all() assert len(results) == 2 assert results[0].operation == db.Snapshot.OPERATION_CREATED @@ -79,10 +76,8 @@ def test_not_adding_snapshot_if_data_doesnt_change( db.session.flush() with fake_datetime('13:00:00'): snapshots.create(tag, user) - db.session.flush() with fake_datetime('14:00:00'): snapshots.modify(tag, user) - db.session.flush() results = db.session.query(db.Snapshot).all() assert len(results) == 1 assert results[0].operation == db.Snapshot.OPERATION_CREATED @@ -96,11 +91,9 @@ def test_not_merging_due_to_time_difference( db.session.flush() with fake_datetime('13:00:00'): snapshots.create(tag, user) - db.session.flush() tag.names = [db.TagName('changed')] with fake_datetime('13:10:01'): snapshots.modify(tag, user) - db.session.flush() assert db.session.query(db.Snapshot).count() == 2 def test_not_merging_operations_by_different_users( @@ -111,10 +104,8 @@ def test_not_merging_operations_by_different_users( db.session.flush() with fake_datetime('13:00:00'): snapshots.create(tag, user1) - db.session.flush() tag.names = [db.TagName('changed')] snapshots.modify(tag, user2) - db.session.flush() assert db.session.query(db.Snapshot).count() == 2 def test_merging_resets_merging_time_window( @@ -125,15 +116,12 @@ def test_merging_resets_merging_time_window( db.session.flush() with fake_datetime('13:00:00'): snapshots.create(tag, user) - db.session.flush() tag.names = [db.TagName('changed')] with fake_datetime('13:09:59'): snapshots.modify(tag, user) - db.session.flush() tag.names = [db.TagName('changed again')] with fake_datetime('13:19:59'): snapshots.modify(tag, user) - db.session.flush() results = db.session.query(db.Snapshot).all() assert len(results) == 1 assert results[0].data['names'] == ['changed again'] @@ -148,22 +136,24 @@ def test_merging_deletion_to_modification_or_creation( db.session.flush() with fake_datetime('13:00:00'): initial_operation(tag, user) - db.session.flush() tag.names = [db.TagName('changed')] with fake_datetime('14:00:00'): snapshots.modify(tag, user) - db.session.flush() tag.names = [db.TagName('changed again')] with fake_datetime('14:00:01'): snapshots.delete(tag, user) - db.session.flush() assert db.session.query(db.Snapshot).count() == 2 results = db.session \ .query(db.Snapshot) \ .order_by(db.Snapshot.snapshot_id.asc()) \ .all() assert results[1].operation == db.Snapshot.OPERATION_DELETED - assert results[1].data == {'names': ['changed again'], 'category': 'dummy'} + assert results[1].data == { + 'names': ['changed again'], + 'category': 'dummy', + 'suggestions': [], + 'implications': [], + } @pytest.mark.parametrize( 'expected_operation', [snapshots.create, snapshots.modify]) @@ -175,13 +165,41 @@ def test_merging_deletion_all_the_way_deletes_all_snapshots( db.session.flush() with fake_datetime('13:00:00'): snapshots.create(tag, user) - db.session.flush() tag.names = [db.TagName('changed')] with fake_datetime('13:00:01'): snapshots.modify(tag, user) - db.session.flush() tag.names = [db.TagName('changed again')] with fake_datetime('13:00:02'): snapshots.delete(tag, user) - db.session.flush() assert db.session.query(db.Snapshot).count() == 0 + +def test_get_data(fake_datetime, tag_factory, user_factory): + tag = tag_factory(names=['dummy']) + user = user_factory() + db.session.add_all([tag, user]) + db.session.flush() + with fake_datetime('2016-04-19 13:00:00'): + snapshots.create(tag, user) + tag.names = [db.TagName('changed')] + with fake_datetime('2016-04-19 13:10:01'): + snapshots.modify(tag, user) + assert snapshots.get_data(tag) == [ + { + 'time': datetime.datetime(2016, 4, 19, 13, 10, 1), + 'data': { + 'names': ['changed'], + 'category': 'dummy', + 'suggestions': [], + 'implications': [], + }, + }, + { + 'time': datetime.datetime(2016, 4, 19, 13, 0, 0), + 'data': { + 'names': ['dummy'], + 'category': 'dummy', + 'suggestions': [], + 'implications': [], + }, + }, + ] diff --git a/server/szurubooru/util/snapshots.py b/server/szurubooru/util/snapshots.py index 4e87708..5d366ab 100644 --- a/server/szurubooru/util/snapshots.py +++ b/server/szurubooru/util/snapshots.py @@ -5,12 +5,10 @@ from szurubooru import db def get_tag_snapshot(tag): ret = { 'names': [tag_name.name for tag_name in tag.names], - 'category': tag.category.name + 'category': tag.category.name, + 'suggestions': sorted(rel.first_name for rel in tag.suggestions), + 'implications': sorted(rel.first_name for rel in tag.implications), } - if tag.suggestions: - ret['suggestions'] = sorted(rel.first_name for rel in tag.suggestions) - if tag.implications: - ret['implications'] = sorted(rel.first_name for rel in tag.implications) return ret def get_tag_category_snapshot(category): @@ -25,13 +23,32 @@ serializers = { 'tag_category': get_tag_category_snapshot, } -def save(operation, entity, auth_user): +def get_resource_info(entity): table_name = entity.__table__.name primary_key = inspect(entity).identity assert table_name in serializers assert primary_key is not None assert len(primary_key) == 1 primary_key = primary_key[0] + return (table_name, primary_key) + +def get_snapshots(entity): + table_name, primary_key = get_resource_info(entity) + return db.session \ + .query(db.Snapshot) \ + .filter(db.Snapshot.resource_type == table_name) \ + .filter(db.Snapshot.resource_id == primary_key) \ + .order_by(db.Snapshot.creation_time.desc()) \ + .all() + +def get_data(entity): + ret = [] + for snapshot in get_snapshots(entity): + ret.append({'data': snapshot.data, 'time': snapshot.creation_time}) + return ret + +def save(operation, entity, auth_user): + table_name, primary_key = get_resource_info(entity) now = datetime.datetime.now() snapshot = db.Snapshot() @@ -42,12 +59,7 @@ def save(operation, entity, auth_user): snapshot.data = serializers[table_name](entity) snapshot.user = auth_user - earlier_snapshots = db.session \ - .query(db.Snapshot) \ - .filter(db.Snapshot.resource_type == table_name) \ - .filter(db.Snapshot.resource_id == primary_key) \ - .order_by(db.Snapshot.creation_time.desc()) \ - .all() + earlier_snapshots = get_snapshots(entity) delta = datetime.timedelta(minutes=10) snapshots_left = len(earlier_snapshots)