server/snapshots: add snapshots to tags
This commit is contained in:
parent
1c064778c6
commit
9350c4ff97
5 changed files with 272 additions and 1 deletions
|
@ -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 {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
20
server/szurubooru/db/snapshot.py
Normal file
20
server/szurubooru/db/snapshot.py
Normal 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')
|
180
server/szurubooru/tests/util/test_snapshots.py
Normal file
180
server/szurubooru/tests/util/test_snapshots.py
Normal 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
|
67
server/szurubooru/util/snapshots.py
Normal file
67
server/szurubooru/util/snapshots.py
Normal 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)
|
Loading…
Reference in a new issue