mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 22:42:25 -04:00

The undo code was using triggers and a temporary table to write out all changed rows before making a change. This made for powerful undo/redo support, but had some problems: - creating the tables and triggers wasn't cheap, especially on mobile devices - likewise, every data modification required writing into two separate databases, almost doubling the amount of writes required - it was possible to leave the DB in an inconsistent state if an undoable operation is followed by a non-undoable operation that references the undoable operation, and the user then rolls back the undoable operation. To address these issues, we simplify undo by integrating it with the autosave changes: - .save() can be passed a name to mark a rollback point. If the user undoes the change, any changes since the last save are lost - autosaves happen every 5 minutes, and are pushed back on a .save(), so the maximum work a user can lose is 5 minutes. - reviews are handled separately, so we can let the user undo multiple reviews at once - if necessary, special cases could be added for other operations like marking This means that if a user does two damaging operations in a row they won't be able to restore the first one, but such an event is both unlikely, and is also covered by the backups made each time a deck is opened.
89 lines
2.2 KiB
Python
89 lines
2.2 KiB
Python
# coding: utf-8
|
|
|
|
import time
|
|
from tests.shared import assertException, getEmptyDeck
|
|
from anki.stdmodels import BasicModel
|
|
|
|
def test_op():
|
|
d = getEmptyDeck()
|
|
# should have no undo by default
|
|
assert not d.undoName()
|
|
# let's adjust a study option
|
|
assert d.qconf['repLim'] == 0
|
|
d.save("studyopts")
|
|
d.qconf['repLim'] = 10
|
|
# it should be listed as undoable
|
|
assert d.undoName() == "studyopts"
|
|
# with about 5 minutes until it's clobbered
|
|
assert time.time() - d._lastSave < 1
|
|
# undoing should restore the old value
|
|
d.undo()
|
|
assert not d.undoName()
|
|
assert d.qconf['repLim'] == 0
|
|
# an (auto)save will clear the undo
|
|
d.save("foo")
|
|
assert d.undoName() == "foo"
|
|
d.save()
|
|
assert not d.undoName()
|
|
# and a review will, too
|
|
d.save("add")
|
|
f = d.newFact()
|
|
f['Front'] = u"one"
|
|
d.addFact(f)
|
|
d.reset()
|
|
assert d.undoName() == "add"
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 2)
|
|
assert d.undoName() == "Review"
|
|
|
|
def test_review():
|
|
d = getEmptyDeck()
|
|
f = d.newFact()
|
|
f['Front'] = u"one"
|
|
d.addFact(f)
|
|
d.reset()
|
|
assert not d.undoName()
|
|
# answer
|
|
assert d.sched.counts() == (1, 0, 0)
|
|
c = d.sched.getCard()
|
|
assert c.queue == 0
|
|
assert c.grade == 0
|
|
d.sched.answerCard(c, 2)
|
|
assert d.sched.counts() == (0, 1, 0)
|
|
assert c.queue == 1
|
|
assert c.grade == 1
|
|
# undo
|
|
assert d.undoName()
|
|
d.undo()
|
|
d.reset()
|
|
assert d.sched.counts() == (1, 0, 0)
|
|
c.load()
|
|
assert c.queue == 0
|
|
assert c.grade == 0
|
|
assert not d.undoName()
|
|
# we should be able to undo multiple answers too
|
|
f['Front'] = u"two"
|
|
d.addFact(f)
|
|
d.reset()
|
|
assert d.sched.counts() == (2, 0, 0)
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 2)
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 2)
|
|
assert d.sched.counts() == (0, 2, 0)
|
|
d.undo()
|
|
d.reset()
|
|
assert d.sched.counts() == (1, 1, 0)
|
|
d.undo()
|
|
d.reset()
|
|
assert d.sched.counts() == (2, 0, 0)
|
|
# performing a normal op will clear the review queue
|
|
c = d.sched.getCard()
|
|
d.sched.answerCard(c, 2)
|
|
assert d.undoName() == "Review"
|
|
d.save("foo")
|
|
assert d.undoName() == "foo"
|
|
d.undo()
|
|
assert not d.undoName()
|
|
|
|
|