mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
preserve learning/filtered state when suspending/burying
add new card type of 3 so we can distinguish cards in relearning from normal reviews
This commit is contained in:
parent
ba87fc7736
commit
62c1fa4a17
2 changed files with 66 additions and 80 deletions
109
anki/sched.py
109
anki/sched.py
|
@ -14,7 +14,9 @@ from anki.lang import _
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.hooks import runHook
|
from anki.hooks import runHook
|
||||||
|
|
||||||
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
|
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
||||||
|
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
|
||||||
|
# 4=preview, -1=suspended, -2=buried
|
||||||
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
|
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
|
||||||
# positive revlog intervals are in days (rev), negative in seconds (lrn)
|
# positive revlog intervals are in days (rev), negative in seconds (lrn)
|
||||||
# odue/odid store original due/did when cards moved to filtered deck
|
# odue/odid store original due/did when cards moved to filtered deck
|
||||||
|
@ -147,23 +149,6 @@ order by due""" % self._deckLimit(),
|
||||||
return 2
|
return 2
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
def unburyCards(self):
|
|
||||||
"Unbury cards."
|
|
||||||
self.col.conf['lastUnburied'] = self.today
|
|
||||||
self.col.log(
|
|
||||||
self.col.db.list("select id from cards where queue = -2"))
|
|
||||||
self.col.db.execute(
|
|
||||||
"update cards set queue=type where queue = -2")
|
|
||||||
|
|
||||||
def unburyCardsForDeck(self):
|
|
||||||
sids = ids2str(self.col.decks.active())
|
|
||||||
self.col.log(
|
|
||||||
self.col.db.list("select id from cards where queue = -2 and did in %s"
|
|
||||||
% sids))
|
|
||||||
self.col.db.execute(
|
|
||||||
"update cards set mod=?,usn=?,queue=type where queue = -2 and did in %s"
|
|
||||||
% sids, intTime(), self.col.usn())
|
|
||||||
|
|
||||||
# Rev/lrn/time daily stats
|
# Rev/lrn/time daily stats
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -562,19 +547,17 @@ did = ? and queue = 3 and due <= ? limit ?""",
|
||||||
card.left = self._startingLeft(card)
|
card.left = self._startingLeft(card)
|
||||||
|
|
||||||
card.lastIvl = card.ivl
|
card.lastIvl = card.ivl
|
||||||
if card.type == 2:
|
|
||||||
# review card that will move to relearning
|
# relearning card?
|
||||||
|
if card.type in (2,3):
|
||||||
card.ivl = self._lapseIvl(card, self._lrnConf(card))
|
card.ivl = self._lapseIvl(card, self._lrnConf(card))
|
||||||
|
|
||||||
if card.queue not in (1,3):
|
# moving from review queue?
|
||||||
|
if card.type == 2:
|
||||||
|
card.type = 3
|
||||||
card.lapses += 1
|
card.lapses += 1
|
||||||
card.factor = max(1300, card.factor-200)
|
card.factor = max(1300, card.factor-200)
|
||||||
|
|
||||||
# if no relearning steps, reschedule as review immediately
|
|
||||||
if not conf['delays']:
|
|
||||||
self._rescheduleAsRev(card, conf, False)
|
|
||||||
return
|
|
||||||
|
|
||||||
return self._rescheduleLrnCard(card, conf)
|
return self._rescheduleLrnCard(card, conf)
|
||||||
|
|
||||||
def _moveToNextStep(self, card, conf):
|
def _moveToNextStep(self, card, conf):
|
||||||
|
@ -634,13 +617,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
|
||||||
return avg
|
return avg
|
||||||
|
|
||||||
def _lrnConf(self, card):
|
def _lrnConf(self, card):
|
||||||
if card.type == 2:
|
if card.type in (2, 3):
|
||||||
return self._lapseConf(card)
|
return self._lapseConf(card)
|
||||||
else:
|
else:
|
||||||
return self._newConf(card)
|
return self._newConf(card)
|
||||||
|
|
||||||
def _rescheduleAsRev(self, card, conf, early):
|
def _rescheduleAsRev(self, card, conf, early):
|
||||||
lapse = card.type == 2
|
lapse = card.type in (2,3)
|
||||||
|
|
||||||
if lapse:
|
if lapse:
|
||||||
self._rescheduleGraduatingLapse(card)
|
self._rescheduleGraduatingLapse(card)
|
||||||
|
@ -654,6 +637,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
|
||||||
def _rescheduleGraduatingLapse(self, card):
|
def _rescheduleGraduatingLapse(self, card):
|
||||||
card.due = self.today+card.ivl
|
card.due = self.today+card.ivl
|
||||||
card.queue = 2
|
card.queue = 2
|
||||||
|
card.type = 2
|
||||||
|
|
||||||
def _startingLeft(self, card):
|
def _startingLeft(self, card):
|
||||||
if card.type == 2:
|
if card.type == 2:
|
||||||
|
@ -712,26 +696,6 @@ did = ? and queue = 3 and due <= ? limit ?""",
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
log()
|
log()
|
||||||
|
|
||||||
def removeLrn(self, ids=None):
|
|
||||||
"Remove cards from the learning queues."
|
|
||||||
if ids:
|
|
||||||
extra = " and id in "+ids2str(ids)
|
|
||||||
else:
|
|
||||||
# benchmarks indicate it's about 10x faster to search all decks
|
|
||||||
# with the index than scan the table
|
|
||||||
extra = " and did in "+ids2str(self.col.decks.allIds())
|
|
||||||
# review cards in relearning
|
|
||||||
self.col.db.execute("""
|
|
||||||
update cards set
|
|
||||||
due = ivl+?, queue = 2, mod = %d, usn = %d,
|
|
||||||
odue = 0 -- older anki versions set odue when cards in relearning
|
|
||||||
where queue in (1,3) and type = 2
|
|
||||||
%s
|
|
||||||
""" % (intTime(), self.col.usn(), extra), self.today)
|
|
||||||
# new cards in learning
|
|
||||||
self.forgetCards(self.col.db.list(
|
|
||||||
"select id from cards where queue in (1,3) %s" % extra))
|
|
||||||
|
|
||||||
def _lrnForDeck(self, did):
|
def _lrnForDeck(self, did):
|
||||||
cnt = self.col.db.scalar(
|
cnt = self.col.db.scalar(
|
||||||
"""
|
"""
|
||||||
|
@ -846,7 +810,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
|
||||||
|
|
||||||
def _rescheduleLapse(self, card):
|
def _rescheduleLapse(self, card):
|
||||||
conf = self._lapseConf(card)
|
conf = self._lapseConf(card)
|
||||||
delay = self._moveToFirstStep(card, conf)
|
if conf['delays']:
|
||||||
|
delay = self._moveToFirstStep(card, conf)
|
||||||
|
else:
|
||||||
|
# no relearning steps
|
||||||
|
self._rescheduleAsRev(card, conf, early=False)
|
||||||
|
|
||||||
# if suspended as a leech, nothing to do
|
# if suspended as a leech, nothing to do
|
||||||
if self._checkLeech(card, conf) and card.queue == -1:
|
if self._checkLeech(card, conf) and card.queue == -1:
|
||||||
|
@ -1033,9 +1001,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
|
||||||
lim = "did = %s" % did
|
lim = "did = %s" % did
|
||||||
self.col.log(self.col.db.list("select id from cards where %s" % lim))
|
self.col.log(self.col.db.list("select id from cards where %s" % lim))
|
||||||
|
|
||||||
|
# update queue in preview case
|
||||||
self.col.db.execute("""
|
self.col.db.execute("""
|
||||||
update cards set did = odid,
|
update cards set did = odid, %s,
|
||||||
due = odue, odue = 0, odid = 0, usn = ? where %s""" % lim,
|
due = odue, odue = 0, odid = 0, usn = ? where %s""" % (
|
||||||
|
self._restoreQueueSnippet, lim),
|
||||||
self.col.usn())
|
self.col.usn())
|
||||||
|
|
||||||
def remFromDyn(self, cids):
|
def remFromDyn(self, cids):
|
||||||
|
@ -1346,14 +1316,22 @@ To study outside of the normal schedule, click the Custom Study button below."""
|
||||||
else:
|
else:
|
||||||
return self._delayForGrade(conf, left)
|
return self._delayForGrade(conf, left)
|
||||||
|
|
||||||
# Suspending
|
# Suspending & burying
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
# learning and relearning cards may be seconds-based or day-based;
|
||||||
|
# other types map directly to queues
|
||||||
|
_restoreQueueSnippet = """
|
||||||
|
queue = (case when type in (1,3) then
|
||||||
|
(case when (case when odue then odue else due end) > 1000000000 then 1 else 3 end)
|
||||||
|
else
|
||||||
|
type
|
||||||
|
end)
|
||||||
|
"""
|
||||||
|
|
||||||
def suspendCards(self, ids):
|
def suspendCards(self, ids):
|
||||||
"Suspend cards."
|
"Suspend cards."
|
||||||
self.col.log(ids)
|
self.col.log(ids)
|
||||||
self.remFromDyn(ids)
|
|
||||||
self.removeLrn(ids)
|
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
"update cards set queue=-1,mod=?,usn=? where id in "+
|
"update cards set queue=-1,mod=?,usn=? where id in "+
|
||||||
ids2str(ids), intTime(), self.col.usn())
|
ids2str(ids), intTime(), self.col.usn())
|
||||||
|
@ -1362,14 +1340,12 @@ To study outside of the normal schedule, click the Custom Study button below."""
|
||||||
"Unsuspend cards."
|
"Unsuspend cards."
|
||||||
self.col.log(ids)
|
self.col.log(ids)
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
"update cards set queue=type,mod=?,usn=? "
|
("update cards set %s,mod=?,usn=? "
|
||||||
"where queue = -1 and id in "+ ids2str(ids),
|
"where queue = -1 and id in %s") % (self._restoreQueueSnippet, ids2str(ids)),
|
||||||
intTime(), self.col.usn())
|
intTime(), self.col.usn())
|
||||||
|
|
||||||
def buryCards(self, cids):
|
def buryCards(self, cids):
|
||||||
self.col.log(cids)
|
self.col.log(cids)
|
||||||
self.remFromDyn(cids)
|
|
||||||
self.removeLrn(cids)
|
|
||||||
self.col.db.execute("""
|
self.col.db.execute("""
|
||||||
update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids),
|
update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids),
|
||||||
intTime(), self.col.usn())
|
intTime(), self.col.usn())
|
||||||
|
@ -1380,6 +1356,23 @@ update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids),
|
||||||
"select id from cards where nid = ? and queue >= 0", nid)
|
"select id from cards where nid = ? and queue >= 0", nid)
|
||||||
self.buryCards(cids)
|
self.buryCards(cids)
|
||||||
|
|
||||||
|
def unburyCards(self):
|
||||||
|
"Unbury cards."
|
||||||
|
self.col.conf['lastUnburied'] = self.today
|
||||||
|
self.col.log(
|
||||||
|
self.col.db.list("select id from cards where queue = -2"))
|
||||||
|
self.col.db.execute(
|
||||||
|
"update cards set %s where queue = -2" % self._restoreQueueSnippet)
|
||||||
|
|
||||||
|
def unburyCardsForDeck(self):
|
||||||
|
sids = ids2str(self.col.decks.active())
|
||||||
|
self.col.log(
|
||||||
|
self.col.db.list("select id from cards where queue = -2 and did in %s"
|
||||||
|
% sids))
|
||||||
|
self.col.db.execute(
|
||||||
|
"update cards set mod=?,usn=?,%s where queue = -2 and did in %s"
|
||||||
|
% (self._restoreQueueSnippet, sids), intTime(), self.col.usn())
|
||||||
|
|
||||||
# Sibling spacing
|
# Sibling spacing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
|
@ -182,7 +182,7 @@ def test_relearn():
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
assert c.type == 2
|
assert c.type == 3
|
||||||
assert c.ivl == 1
|
assert c.ivl == 1
|
||||||
|
|
||||||
# immediately graduate it
|
# immediately graduate it
|
||||||
|
@ -191,17 +191,6 @@ def test_relearn():
|
||||||
assert c.ivl == 1
|
assert c.ivl == 1
|
||||||
assert c.due == d.sched.today + c.ivl
|
assert c.due == d.sched.today + c.ivl
|
||||||
|
|
||||||
# if forced out of learning, it should have the correct due date
|
|
||||||
c.ivl = 100
|
|
||||||
c.due = d.sched.today
|
|
||||||
c.flush()
|
|
||||||
c = d.sched.getCard()
|
|
||||||
d.sched.answerCard(c, 1)
|
|
||||||
assert c.due > intTime()
|
|
||||||
d.sched.removeLrn([c.id])
|
|
||||||
c.load()
|
|
||||||
assert c.due == d.sched.today + c.ivl
|
|
||||||
|
|
||||||
def test_learn_collapsed():
|
def test_learn_collapsed():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add 2 notes
|
# add 2 notes
|
||||||
|
@ -525,14 +514,15 @@ def test_suspend():
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.due >= time.time()
|
assert c.due >= time.time()
|
||||||
|
due = c.due
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
assert c.type == 2
|
assert c.type == 3
|
||||||
d.sched.suspendCards([c.id])
|
d.sched.suspendCards([c.id])
|
||||||
d.sched.unsuspendCards([c.id])
|
d.sched.unsuspendCards([c.id])
|
||||||
c.load()
|
c.load()
|
||||||
assert c.queue == 2
|
assert c.queue == 1
|
||||||
assert c.type == 2
|
assert c.type == 3
|
||||||
assert c.due == 1
|
assert c.due == due
|
||||||
# should cope with cards in cram decks
|
# should cope with cards in cram decks
|
||||||
c.due = 1
|
c.due = 1
|
||||||
c.flush()
|
c.flush()
|
||||||
|
@ -543,8 +533,9 @@ def test_suspend():
|
||||||
assert c.did != 1
|
assert c.did != 1
|
||||||
d.sched.suspendCards([c.id])
|
d.sched.suspendCards([c.id])
|
||||||
c.load()
|
c.load()
|
||||||
assert c.due == 1
|
assert c.due != 1
|
||||||
assert c.did == 1
|
assert c.did != 1
|
||||||
|
assert c.odue == 1
|
||||||
|
|
||||||
def test_filt_reviewing_early_normal():
|
def test_filt_reviewing_early_normal():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
|
@ -660,15 +651,17 @@ def test_preview():
|
||||||
|
|
||||||
# passing it will remove it
|
# passing it will remove it
|
||||||
d.sched.answerCard(c2, 2)
|
d.sched.answerCard(c2, 2)
|
||||||
|
assert c2.queue == 0
|
||||||
|
assert c2.reps == 0
|
||||||
|
assert c2.type == 0
|
||||||
|
|
||||||
# the other card should appear again
|
# the other card should appear again
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert c.id == orig.id
|
assert c.id == orig.id
|
||||||
|
|
||||||
# remove it
|
# emptying the filtered deck should restore card
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.emptyDyn(did)
|
||||||
|
c.load()
|
||||||
# ensure it's in the same state as it started
|
|
||||||
assert c.queue == 0
|
assert c.queue == 0
|
||||||
assert c.reps == 0
|
assert c.reps == 0
|
||||||
assert c.type == 0
|
assert c.type == 0
|
||||||
|
|
Loading…
Reference in a new issue