server/snapshots: add snapshots to tags

This commit is contained in:
rr- 2016-04-18 20:44:39 +02:00
parent 1c064778c6
commit 9350c4ff97
5 changed files with 272 additions and 1 deletions

View file

@ -1,6 +1,6 @@
import datetime import datetime
from szurubooru import search from szurubooru import search
from szurubooru.util import auth, tags from szurubooru.util import auth, tags, snapshots
from szurubooru.api.base_api import BaseApi from szurubooru.api.base_api import BaseApi
def _serialize_tag(tag): def _serialize_tag(tag):
@ -49,6 +49,7 @@ class TagListApi(BaseApi):
ctx.session.add(tag) ctx.session.add(tag)
ctx.session.commit() ctx.session.commit()
tags.export_to_json(ctx.session) tags.export_to_json(ctx.session)
snapshots.create(ctx.session, tag, ctx.user)
return {'tag': _serialize_tag(tag)} return {'tag': _serialize_tag(tag)}
class TagDetailApi(BaseApi): class TagDetailApi(BaseApi):
@ -86,6 +87,7 @@ class TagDetailApi(BaseApi):
tag.last_edit_time = datetime.datetime.now() tag.last_edit_time = datetime.datetime.now()
ctx.session.commit() ctx.session.commit()
tags.export_to_json(ctx.session) tags.export_to_json(ctx.session)
snapshots.modify(ctx.session, tag, ctx.user)
return {'tag': _serialize_tag(tag)} return {'tag': _serialize_tag(tag)}
def delete(self, ctx, tag_name): def delete(self, ctx, tag_name):
@ -100,5 +102,6 @@ class TagDetailApi(BaseApi):
auth.verify_privilege(ctx.user, 'tags:delete') auth.verify_privilege(ctx.user, 'tags:delete')
ctx.session.delete(tag) ctx.session.delete(tag)
ctx.session.commit() ctx.session.commit()
snapshots.delete(ctx.session, tag, ctx.user)
tags.export_to_json(ctx.session) tags.export_to_json(ctx.session)
return {} return {}

View file

@ -2,3 +2,4 @@ from szurubooru.db.base import Base
from szurubooru.db.user import User from szurubooru.db.user import User
from szurubooru.db.tag import Tag, TagName, TagSuggestion, TagImplication from szurubooru.db.tag import Tag, TagName, TagSuggestion, TagImplication
from szurubooru.db.post import Post, PostTag, PostRelation from szurubooru.db.post import Post, PostTag, PostRelation
from szurubooru.db.snapshot import Snapshot

View file

@ -0,0 +1,20 @@
from sqlalchemy import Column, Integer, DateTime, String, PickleType, ForeignKey
from sqlalchemy.orm import relationship
from szurubooru.db.base import Base
class Snapshot(Base):
__tablename__ = 'snapshot'
OPERATION_CREATED = 'added'
OPERATION_MODIFIED = 'modified'
OPERATION_DELETED = 'deleted'
snapshot_id = Column('id', Integer, primary_key=True)
creation_time = Column('creation_time', DateTime, nullable=False)
resource_type = Column('resource_type', String(32), nullable=False)
resource_id = Column('resource_id', Integer, nullable=False)
operation = Column('operation', String(16), nullable=False)
user_id = Column('user_id', Integer, ForeignKey('user.id'))
data = Column('data', PickleType)
user = relationship('User')

View file

@ -0,0 +1,180 @@
import datetime
import pytest
from szurubooru import db
from szurubooru.util import snapshots
def test_serializing_tag(session, tag_factory):
tag = tag_factory(names=['main_name', 'alias'], category='dummy')
assert snapshots.get_tag_snapshot(tag) == {
'names': ['main_name', 'alias'],
'category': 'dummy'
}
tag = tag_factory(names=['main_name', 'alias'], category='dummy')
imp1 = tag_factory(names=['imp1_main_name', 'imp1_alias'])
imp2 = tag_factory(names=['imp2_main_name', 'imp2_alias'])
sug1 = tag_factory(names=['sug1_main_name', 'sug1_alias'])
sug2 = tag_factory(names=['sug2_main_name', 'sug2_alias'])
session.add_all([imp1, imp2, sug1, sug2])
tag.implications = [imp1, imp2]
tag.suggestions = [sug1, sug2]
session.flush()
assert snapshots.get_tag_snapshot(tag) == {
'names': ['main_name', 'alias'],
'category': 'dummy',
'implications': ['imp1_main_name', 'imp2_main_name'],
'suggestions': ['sug1_main_name', 'sug2_main_name'],
}
def test_merging_modification_to_creation(session, tag_factory, user_factory):
tag = tag_factory(names=['dummy'], category='dummy')
user = user_factory()
session.add_all([tag, user])
session.flush()
snapshots.create(session, tag, user)
session.flush()
tag.names = [db.TagName('changed')]
snapshots.modify(session, tag, user)
session.flush()
results = session.query(db.Snapshot).all()
assert len(results) == 1
assert results[0].operation == db.Snapshot.OPERATION_CREATED
assert results[0].data['names'] == ['changed']
def test_merging_modifications(
fake_datetime, session, tag_factory, user_factory):
tag = tag_factory(names=['dummy'], category='dummy')
user = user_factory()
session.add_all([tag, user])
session.flush()
with fake_datetime('13:00:00'):
snapshots.create(session, tag, user)
session.flush()
tag.names = [db.TagName('changed')]
with fake_datetime('14:00:00'):
snapshots.modify(session, tag, user)
session.flush()
tag.names = [db.TagName('changed again')]
with fake_datetime('14:00:01'):
snapshots.modify(session, tag, user)
session.flush()
results = session.query(db.Snapshot).all()
assert len(results) == 2
assert results[0].operation == db.Snapshot.OPERATION_CREATED
assert results[1].operation == db.Snapshot.OPERATION_MODIFIED
assert results[0].data['names'] == ['dummy']
assert results[1].data['names'] == ['changed again']
def test_not_adding_snapshot_if_data_doesnt_change(
fake_datetime, session, tag_factory, user_factory):
tag = tag_factory(names=['dummy'], category='dummy')
user = user_factory()
session.add_all([tag, user])
session.flush()
with fake_datetime('13:00:00'):
snapshots.create(session, tag, user)
session.flush()
with fake_datetime('14:00:00'):
snapshots.modify(session, tag, user)
session.flush()
results = session.query(db.Snapshot).all()
assert len(results) == 1
assert results[0].operation == db.Snapshot.OPERATION_CREATED
assert results[0].data['names'] == ['dummy']
def test_not_merging_due_to_time_difference(
fake_datetime, session, tag_factory, user_factory):
tag = tag_factory(names=['dummy'], category='dummy')
user = user_factory()
session.add_all([tag, user])
session.flush()
with fake_datetime('13:00:00'):
snapshots.create(session, tag, user)
session.flush()
tag.names = [db.TagName('changed')]
with fake_datetime('13:10:01'):
snapshots.modify(session, tag, user)
session.flush()
assert session.query(db.Snapshot).count() == 2
def test_not_merging_operations_by_different_users(
fake_datetime, session, tag_factory, user_factory):
tag = tag_factory(names=['dummy'], category='dummy')
user1, user2 = [user_factory(), user_factory()]
session.add_all([tag, user1, user2])
session.flush()
with fake_datetime('13:00:00'):
snapshots.create(session, tag, user1)
session.flush()
tag.names = [db.TagName('changed')]
snapshots.modify(session, tag, user2)
session.flush()
assert session.query(db.Snapshot).count() == 2
def test_merging_resets_merging_time_window(
fake_datetime, session, tag_factory, user_factory):
tag = tag_factory(names=['dummy'], category='dummy')
user = user_factory()
session.add_all([tag, user])
session.flush()
with fake_datetime('13:00:00'):
snapshots.create(session, tag, user)
session.flush()
tag.names = [db.TagName('changed')]
with fake_datetime('13:09:59'):
snapshots.modify(session, tag, user)
session.flush()
tag.names = [db.TagName('changed again')]
with fake_datetime('13:19:59'):
snapshots.modify(session, tag, user)
session.flush()
results = session.query(db.Snapshot).all()
assert len(results) == 1
assert results[0].data['names'] == ['changed again']
@pytest.mark.parametrize(
'initial_operation', [snapshots.create, snapshots.modify])
def test_merging_deletion_to_modification_or_creation(
fake_datetime, session, tag_factory, user_factory, initial_operation):
tag = tag_factory(names=['dummy'], category='dummy')
user = user_factory()
session.add_all([tag, user])
session.flush()
with fake_datetime('13:00:00'):
initial_operation(session, tag, user)
session.flush()
tag.names = [db.TagName('changed')]
with fake_datetime('14:00:00'):
snapshots.modify(session, tag, user)
session.flush()
tag.names = [db.TagName('changed again')]
with fake_datetime('14:00:01'):
snapshots.delete(session, tag, user)
session.flush()
assert session.query(db.Snapshot).count() == 2
results = 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'}
@pytest.mark.parametrize(
'expected_operation', [snapshots.create, snapshots.modify])
def test_merging_deletion_all_the_way_deletes_all_snapshots(
fake_datetime, session, tag_factory, user_factory, expected_operation):
tag = tag_factory(names=['dummy'], category='dummy')
user = user_factory()
session.add_all([tag, user])
session.flush()
with fake_datetime('13:00:00'):
snapshots.create(session, tag, user)
session.flush()
tag.names = [db.TagName('changed')]
with fake_datetime('13:00:01'):
snapshots.modify(session, tag, user)
session.flush()
tag.names = [db.TagName('changed again')]
with fake_datetime('13:00:02'):
snapshots.delete(session, tag, user)
session.flush()
assert session.query(db.Snapshot).count() == 0

View file

@ -0,0 +1,67 @@
import datetime
from sqlalchemy.inspection import inspect
from szurubooru import db
def get_tag_snapshot(tag):
ret = {
'names': [tag_name.name for tag_name in tag.names],
'category': tag.category
}
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
serializers = {
'tag': get_tag_snapshot,
}
def save(session, operation, entity, auth_user):
table_name = entity.__table__.name
primary_key = inspect(entity).identity
assert table_name in serializers
assert len(primary_key) == 1
primary_key = primary_key[0]
now = datetime.datetime.now()
snapshot = db.Snapshot()
snapshot.creation_time = now
snapshot.operation = operation
snapshot.resource_type = table_name
snapshot.resource_id = primary_key
snapshot.data = serializers[table_name](entity)
snapshot.user = auth_user
earlier_snapshots = 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()
delta = datetime.timedelta(minutes=10)
snapshots_left = len(earlier_snapshots)
while earlier_snapshots:
last_snapshot = earlier_snapshots.pop(0)
is_fresh = now - last_snapshot.creation_time <= delta
if snapshot.data != last_snapshot.data:
if not is_fresh or last_snapshot.user != auth_user:
break
session.delete(last_snapshot)
if snapshot.operation != db.Snapshot.OPERATION_DELETED:
snapshot.operation = last_snapshot.operation
snapshots_left -= 1
if not snapshots_left and operation == db.Snapshot.OPERATION_DELETED:
pass
else:
session.add(snapshot)
def create(session, entity, auth_user):
save(session, db.Snapshot.OPERATION_CREATED, entity, auth_user)
def modify(session, entity, auth_user):
save(session, db.Snapshot.OPERATION_MODIFIED, entity, auth_user)
def delete(session, entity, auth_user):
save(session, db.Snapshot.OPERATION_DELETED, entity, auth_user)