aefc58d5d5
or they end up with an invalid status when unburied https://anki.lighthouseapp.com/projects/100923/tickets/484-cant-sync
1349 lines
46 KiB
Python
1349 lines
46 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import time, random, itertools
|
|
from operator import itemgetter
|
|
from heapq import *
|
|
#from anki.cards import Card
|
|
from anki.utils import ids2str, intTime, fmtTimeSpan
|
|
from anki.lang import _
|
|
from anki.consts import *
|
|
from anki.hooks import runHook
|
|
|
|
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
|
|
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
|
|
# positive revlog intervals are in days (rev), negative in seconds (lrn)
|
|
|
|
class Scheduler(object):
|
|
name = "std"
|
|
haveCustomStudy = True
|
|
_spreadRev = True
|
|
|
|
def __init__(self, col):
|
|
self.col = col
|
|
self.queueLimit = 50
|
|
self.reportLimit = 1000
|
|
self.reps = 0
|
|
self._haveQueues = False
|
|
self._updateCutoff()
|
|
|
|
def getCard(self):
|
|
"Pop the next card from the queue. None if finished."
|
|
self._checkDay()
|
|
if not self._haveQueues:
|
|
self.reset()
|
|
card = self._getCard()
|
|
if card:
|
|
self.reps += 1
|
|
card.startTimer()
|
|
return card
|
|
|
|
def reset(self):
|
|
deck = self.col.decks.current()
|
|
self._updateCutoff()
|
|
self._resetLrn()
|
|
self._resetRev()
|
|
self._resetNew()
|
|
self._haveQueues = True
|
|
|
|
def answerCard(self, card, ease):
|
|
assert ease >= 1 and ease <= 4
|
|
self.col.markReview(card)
|
|
card.reps += 1
|
|
# former is for logging new cards, latter also covers filt. decks
|
|
card.wasNew = card.type == 0
|
|
wasNewQ = card.queue == 0
|
|
if wasNewQ:
|
|
# came from the new queue, move to learning
|
|
card.queue = 1
|
|
# if it was a new card, it's now a learning card
|
|
if card.type == 0:
|
|
card.type = 1
|
|
# init reps to graduation
|
|
card.left = self._startingLeft(card)
|
|
# dynamic?
|
|
if card.odid and card.type == 2:
|
|
if self._resched(card):
|
|
# reviews get their ivl boosted on first sight
|
|
card.ivl = self._dynIvlBoost(card)
|
|
card.odue = self.today + card.ivl
|
|
self._updateStats(card, 'new')
|
|
if card.queue in (1, 3):
|
|
self._answerLrnCard(card, ease)
|
|
if not wasNewQ:
|
|
self._updateStats(card, 'lrn')
|
|
elif card.queue == 2:
|
|
self._answerRevCard(card, ease)
|
|
self._updateStats(card, 'rev')
|
|
else:
|
|
raise Exception("Invalid queue")
|
|
self._updateStats(card, 'time', card.timeTaken())
|
|
card.mod = intTime()
|
|
card.usn = self.col.usn()
|
|
card.flushSched()
|
|
|
|
def counts(self, card=None):
|
|
counts = [self.newCount, self.lrnCount, self.revCount]
|
|
if card:
|
|
idx = self.countIdx(card)
|
|
if idx == 1:
|
|
counts[1] += card.left/1000
|
|
else:
|
|
counts[idx] += 1
|
|
return tuple(counts)
|
|
|
|
def dueForecast(self, days=7):
|
|
"Return counts over next DAYS. Includes today."
|
|
daysd = dict(self.col.db.all("""
|
|
select due, count() from cards
|
|
where did in %s and queue = 2
|
|
and due between ? and ?
|
|
group by due
|
|
order by due""" % self._deckLimit(),
|
|
self.today,
|
|
self.today+days-1))
|
|
for d in range(days):
|
|
d = self.today+d
|
|
if d not in daysd:
|
|
daysd[d] = 0
|
|
# return in sorted order
|
|
ret = [x[1] for x in sorted(daysd.items())]
|
|
return ret
|
|
|
|
def countIdx(self, card):
|
|
if card.queue == 3:
|
|
return 1
|
|
return card.queue
|
|
|
|
def answerButtons(self, card):
|
|
if card.odue:
|
|
# normal review in dyn deck?
|
|
if card.odid and card.queue == 2:
|
|
return 4
|
|
conf = self._lapseConf(card)
|
|
if card.type == 0 or len(conf['delays']) > 1:
|
|
return 3
|
|
return 2
|
|
elif card.queue == 2:
|
|
return 4
|
|
else:
|
|
return 3
|
|
|
|
def unburyCards(self):
|
|
"Unbury cards when closing."
|
|
mod = self.col.db.mod
|
|
self.col.db.execute(
|
|
"update cards set queue = type where queue = -2")
|
|
self.col.db.mod = mod
|
|
|
|
# Rev/lrn/time daily stats
|
|
##########################################################################
|
|
|
|
def _updateStats(self, card, type, cnt=1):
|
|
key = type+"Today"
|
|
for g in ([self.col.decks.get(card.did)] +
|
|
self.col.decks.parents(card.did)):
|
|
# add
|
|
g[key][1] += cnt
|
|
self.col.decks.save(g)
|
|
|
|
def extendLimits(self, new, rev):
|
|
cur = self.col.decks.current()
|
|
parents = self.col.decks.parents(cur['id'])
|
|
children = [self.col.decks.get(did) for (name, did) in
|
|
self.col.decks.children(cur['id'])]
|
|
for g in [cur] + parents + children:
|
|
# add
|
|
g['newToday'][1] -= new
|
|
g['revToday'][1] -= rev
|
|
self.col.decks.save(g)
|
|
|
|
def _walkingCount(self, limFn=None, cntFn=None):
|
|
tot = 0
|
|
pcounts = {}
|
|
# for each of the active decks
|
|
for did in self.col.decks.active():
|
|
# early alphas were setting the active ids as a str
|
|
did = int(did)
|
|
# get the individual deck's limit
|
|
lim = limFn(self.col.decks.get(did))
|
|
if not lim:
|
|
continue
|
|
# check the parents
|
|
parents = self.col.decks.parents(did)
|
|
for p in parents:
|
|
# add if missing
|
|
if p['id'] not in pcounts:
|
|
pcounts[p['id']] = limFn(p)
|
|
# take minimum of child and parent
|
|
lim = min(pcounts[p['id']], lim)
|
|
# see how many cards we actually have
|
|
cnt = cntFn(did, lim)
|
|
# if non-zero, decrement from parent counts
|
|
for p in parents:
|
|
pcounts[p['id']] -= cnt
|
|
# we may also be a parent
|
|
pcounts[did] = lim - cnt
|
|
# and add to running total
|
|
tot += cnt
|
|
return tot
|
|
|
|
# Deck list
|
|
##########################################################################
|
|
|
|
def deckDueList(self):
|
|
"Returns [deckname, did, rev, lrn, new]"
|
|
self._checkDay()
|
|
self.col.decks.recoverOrphans()
|
|
self.unburyCards()
|
|
decks = self.col.decks.all()
|
|
decks.sort(key=itemgetter('name'))
|
|
lims = {}
|
|
data = []
|
|
def parent(name):
|
|
parts = name.split("::")
|
|
if len(parts) < 2:
|
|
return None
|
|
parts = parts[:-1]
|
|
return "::".join(parts)
|
|
for deck in decks:
|
|
# if we've already seen the exact same deck name, remove the
|
|
# invalid duplicate and reload
|
|
if deck['name'] in lims:
|
|
self.col.decks.rem(deck['id'], cardsToo=False, childrenToo=True)
|
|
return self.deckDueList()
|
|
p = parent(deck['name'])
|
|
# new
|
|
nlim = self._deckNewLimitSingle(deck)
|
|
if p:
|
|
if p not in lims:
|
|
# if parent was missing, this deck is invalid, and we
|
|
# need to reload the deck list
|
|
self.col.decks.rem(deck['id'], cardsToo=False, childrenToo=True)
|
|
return self.deckDueList()
|
|
nlim = min(nlim, lims[p][0])
|
|
new = self._newForDeck(deck['id'], nlim)
|
|
# learning
|
|
lrn = self._lrnForDeck(deck['id'])
|
|
# reviews
|
|
rlim = self._deckRevLimitSingle(deck)
|
|
if p:
|
|
rlim = min(rlim, lims[p][1])
|
|
rev = self._revForDeck(deck['id'], rlim)
|
|
# save to list
|
|
data.append([deck['name'], deck['id'], rev, lrn, new])
|
|
# add deck as a parent
|
|
lims[deck['name']] = [nlim, rlim]
|
|
return data
|
|
|
|
def deckDueTree(self):
|
|
return self._groupChildren(self.deckDueList())
|
|
|
|
def _groupChildren(self, grps):
|
|
# first, split the group names into components
|
|
for g in grps:
|
|
g[0] = g[0].split("::")
|
|
# and sort based on those components
|
|
grps.sort(key=itemgetter(0))
|
|
# then run main function
|
|
return self._groupChildrenMain(grps)
|
|
|
|
def _groupChildrenMain(self, grps):
|
|
tree = []
|
|
# group and recurse
|
|
def key(grp):
|
|
return grp[0][0]
|
|
for (head, tail) in itertools.groupby(grps, key=key):
|
|
tail = list(tail)
|
|
did = None
|
|
rev = 0
|
|
new = 0
|
|
lrn = 0
|
|
children = []
|
|
for c in tail:
|
|
if len(c[0]) == 1:
|
|
# current node
|
|
did = c[1]
|
|
rev += c[2]
|
|
lrn += c[3]
|
|
new += c[4]
|
|
else:
|
|
# set new string to tail
|
|
c[0] = c[0][1:]
|
|
children.append(c)
|
|
children = self._groupChildrenMain(children)
|
|
# tally up children counts
|
|
for ch in children:
|
|
rev += ch[2]
|
|
lrn += ch[3]
|
|
new += ch[4]
|
|
# limit the counts to the deck's limits
|
|
conf = self.col.decks.confForDid(did)
|
|
deck = self.col.decks.get(did)
|
|
if not conf['dyn']:
|
|
rev = max(0, min(rev, conf['rev']['perDay']-deck['revToday'][1]))
|
|
new = max(0, min(new, conf['new']['perDay']-deck['newToday'][1]))
|
|
tree.append((head, did, rev, lrn, new, children))
|
|
return tuple(tree)
|
|
|
|
# Getting the next card
|
|
##########################################################################
|
|
|
|
def _getCard(self):
|
|
"Return the next due card id, or None."
|
|
# learning card due?
|
|
c = self._getLrnCard()
|
|
if c:
|
|
return c
|
|
# new first, or time for one?
|
|
if self._timeForNewCard():
|
|
return self._getNewCard()
|
|
# card due for review?
|
|
c = self._getRevCard()
|
|
if c:
|
|
return c
|
|
# day learning card due?
|
|
c = self._getLrnDayCard()
|
|
if c:
|
|
return c
|
|
# new cards left?
|
|
c = self._getNewCard()
|
|
if c:
|
|
return c
|
|
# collapse or finish
|
|
return self._getLrnCard(collapse=True)
|
|
|
|
# New cards
|
|
##########################################################################
|
|
|
|
def _resetNewCount(self):
|
|
cntFn = lambda did, lim: self.col.db.scalar("""
|
|
select count() from (select 1 from cards where
|
|
did = ? and queue = 0 limit ?)""", did, lim)
|
|
self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn)
|
|
|
|
def _resetNew(self):
|
|
self._resetNewCount()
|
|
self._newDids = self.col.decks.active()[:]
|
|
self._newQueue = []
|
|
self._updateNewCardRatio()
|
|
|
|
def _fillNew(self):
|
|
if self._newQueue:
|
|
return True
|
|
if not self.newCount:
|
|
return False
|
|
while self._newDids:
|
|
did = self._newDids[0]
|
|
lim = min(self.queueLimit, self._deckNewLimit(did))
|
|
if lim:
|
|
# fill the queue with the current did
|
|
self._newQueue = self.col.db.all("""
|
|
select id, due from cards where did = ? and queue = 0 limit ?""", did, lim)
|
|
if self._newQueue:
|
|
self._newQueue.reverse()
|
|
return True
|
|
# nothing left in the deck; move to next
|
|
self._newDids.pop(0)
|
|
|
|
def _getNewCard(self):
|
|
if not self._fillNew():
|
|
return
|
|
(id, due) = self._newQueue.pop()
|
|
# move any siblings to the end?
|
|
conf = self.col.decks.confForDid(self._newDids[0])
|
|
if conf['dyn'] or conf['new']['separate']:
|
|
n = len(self._newQueue)
|
|
while self._newQueue and self._newQueue[-1][1] == due:
|
|
self._newQueue.insert(0, self._newQueue.pop())
|
|
n -= 1
|
|
if not n:
|
|
# we only have one note in the queue; stop rotating
|
|
break
|
|
self.newCount -= 1
|
|
return self.col.getCard(id)
|
|
|
|
def _updateNewCardRatio(self):
|
|
if self.col.conf['newSpread'] == NEW_CARDS_DISTRIBUTE:
|
|
if self.newCount:
|
|
self.newCardModulus = (
|
|
(self.newCount + self.revCount) / self.newCount)
|
|
# if there are cards to review, ensure modulo >= 2
|
|
if self.revCount:
|
|
self.newCardModulus = max(2, self.newCardModulus)
|
|
return
|
|
self.newCardModulus = 0
|
|
|
|
def _timeForNewCard(self):
|
|
"True if it's time to display a new card when distributing."
|
|
if not self.newCount:
|
|
return False
|
|
if self.col.conf['newSpread'] == NEW_CARDS_LAST:
|
|
return False
|
|
elif self.col.conf['newSpread'] == NEW_CARDS_FIRST:
|
|
return True
|
|
elif self.newCardModulus:
|
|
return self.reps and self.reps % self.newCardModulus == 0
|
|
|
|
def _deckNewLimit(self, did, fn=None):
|
|
if not fn:
|
|
fn = self._deckNewLimitSingle
|
|
sel = self.col.decks.get(did)
|
|
lim = -1
|
|
# for the deck and each of its parents
|
|
for g in [sel] + self.col.decks.parents(did):
|
|
rem = fn(g)
|
|
if lim == -1:
|
|
lim = rem
|
|
else:
|
|
lim = min(rem, lim)
|
|
return lim
|
|
|
|
def _newForDeck(self, did, lim):
|
|
"New count for a single deck."
|
|
if not lim:
|
|
return 0
|
|
lim = min(lim, self.reportLimit)
|
|
return self.col.db.scalar("""
|
|
select count() from
|
|
(select 1 from cards where did = ? and queue = 0 limit ?)""", did, lim)
|
|
|
|
def _deckNewLimitSingle(self, g):
|
|
"Limit for deck without parent limits."
|
|
if g['dyn']:
|
|
return self.reportLimit
|
|
c = self.col.decks.confForDid(g['id'])
|
|
return max(0, c['new']['perDay'] - g['newToday'][1])
|
|
|
|
def totalNewForCurrentDeck(self):
|
|
return self.col.db.scalar(
|
|
"""
|
|
select count() from cards where id in (
|
|
select id from cards where did in %s and queue = 0 limit ?)"""
|
|
% ids2str(self.col.decks.active()), self.reportLimit)
|
|
|
|
# Learning queues
|
|
##########################################################################
|
|
|
|
def _resetLrnCount(self):
|
|
# sub-day
|
|
self.lrnCount = self.col.db.scalar("""
|
|
select sum(left/1000) from (select left from cards where
|
|
did in %s and queue = 1 and due < ? limit %d)""" % (
|
|
self._deckLimit(), self.reportLimit),
|
|
self.dayCutoff) or 0
|
|
# day
|
|
self.lrnCount += self.col.db.scalar("""
|
|
select count() from cards where did in %s and queue = 3
|
|
and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit),
|
|
self.today)
|
|
|
|
def _resetLrn(self):
|
|
self._resetLrnCount()
|
|
self._lrnQueue = []
|
|
self._lrnDayQueue = []
|
|
self._lrnDids = self.col.decks.active()[:]
|
|
|
|
# sub-day learning
|
|
def _fillLrn(self):
|
|
if not self.lrnCount:
|
|
return False
|
|
if self._lrnQueue:
|
|
return True
|
|
self._lrnQueue = self.col.db.all("""
|
|
select due, id from cards where
|
|
did in %s and queue = 1 and due < :lim
|
|
limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
|
|
# as it arrives sorted by did first, we need to sort it
|
|
self._lrnQueue.sort()
|
|
return self._lrnQueue
|
|
|
|
def _getLrnCard(self, collapse=False):
|
|
if self._fillLrn():
|
|
cutoff = time.time()
|
|
if collapse:
|
|
cutoff += self.col.conf['collapseTime']
|
|
if self._lrnQueue[0][0] < cutoff:
|
|
id = heappop(self._lrnQueue)[1]
|
|
card = self.col.getCard(id)
|
|
self.lrnCount -= card.left/1000
|
|
return card
|
|
|
|
# daily learning
|
|
def _fillLrnDay(self):
|
|
if not self.lrnCount:
|
|
return False
|
|
if self._lrnDayQueue:
|
|
return True
|
|
while self._lrnDids:
|
|
did = self._lrnDids[0]
|
|
# fill the queue with the current did
|
|
self._lrnDayQueue = self.col.db.list("""
|
|
select id from cards where
|
|
did = ? and queue = 3 and due <= ? limit ?""",
|
|
did, self.today, self.queueLimit)
|
|
if self._lrnDayQueue:
|
|
# order
|
|
r = random.Random()
|
|
r.seed(self.today)
|
|
r.shuffle(self._lrnDayQueue)
|
|
# is the current did empty?
|
|
if len(self._lrnDayQueue) < self.queueLimit:
|
|
self._lrnDids.pop(0)
|
|
return True
|
|
# nothing left in the deck; move to next
|
|
self._lrnDids.pop(0)
|
|
|
|
def _getLrnDayCard(self):
|
|
if self._fillLrnDay():
|
|
self.lrnCount -= 1
|
|
return self.col.getCard(self._lrnDayQueue.pop())
|
|
|
|
def _answerLrnCard(self, card, ease):
|
|
# ease 1=no, 2=yes, 3=remove
|
|
conf = self._lrnConf(card)
|
|
if card.odid and not card.wasNew:
|
|
type = 3
|
|
elif card.type == 2:
|
|
type = 2
|
|
else:
|
|
type = 0
|
|
leaving = False
|
|
# lrnCount was decremented once when card was fetched
|
|
lastLeft = card.left
|
|
# immediate graduate?
|
|
if ease == 3:
|
|
self._rescheduleAsRev(card, conf, True)
|
|
leaving = True
|
|
# graduation time?
|
|
elif ease == 2 and (card.left%1000)-1 <= 0:
|
|
self._rescheduleAsRev(card, conf, False)
|
|
leaving = True
|
|
else:
|
|
# one step towards graduation
|
|
if ease == 2:
|
|
# decrement real left count and recalculate left today
|
|
left = (card.left % 1000) - 1
|
|
card.left = self._leftToday(conf['delays'], left)*1000 + left
|
|
# failed
|
|
else:
|
|
card.left = self._startingLeft(card)
|
|
resched = self._resched(card)
|
|
if 'mult' in conf and resched:
|
|
# review that's lapsed
|
|
card.ivl = max(1, card.ivl*conf['mult'])
|
|
else:
|
|
# new card; no ivl adjustment
|
|
pass
|
|
if resched and card.odid:
|
|
card.odue = self.today + 1
|
|
delay = self._delayForGrade(conf, card.left)
|
|
if card.due < time.time():
|
|
# not collapsed; add some randomness
|
|
delay *= random.uniform(1, 1.25)
|
|
card.due = int(time.time() + delay)
|
|
# due today?
|
|
if card.due < self.dayCutoff:
|
|
self.lrnCount += card.left/1000
|
|
# if the queue is not empty and there's nothing else to do, make
|
|
# sure we don't put it at the head of the queue and end up showing
|
|
# it twice in a row
|
|
card.queue = 1
|
|
if self._lrnQueue and not self.revCount and not self.newCount:
|
|
smallestDue = self._lrnQueue[0][0]
|
|
card.due = max(card.due, smallestDue+1)
|
|
heappush(self._lrnQueue, (card.due, card.id))
|
|
else:
|
|
# the card is due in one or more days, so we need to use the
|
|
# day learn queue
|
|
ahead = ((card.due - self.dayCutoff) / 86400) + 1
|
|
card.due = self.today + ahead
|
|
card.queue = 3
|
|
self._logLrn(card, ease, conf, leaving, type, lastLeft)
|
|
|
|
def _delayForGrade(self, conf, left):
|
|
left = left % 1000
|
|
try:
|
|
delay = conf['delays'][-left]
|
|
except IndexError:
|
|
if conf['delays']:
|
|
delay = conf['delays'][0]
|
|
else:
|
|
# user deleted final step; use dummy value
|
|
delay = 1
|
|
return delay*60
|
|
|
|
def _lrnConf(self, card):
|
|
if card.type == 2:
|
|
return self._lapseConf(card)
|
|
else:
|
|
return self._newConf(card)
|
|
|
|
def _rescheduleAsRev(self, card, conf, early):
|
|
lapse = card.type == 2
|
|
if lapse:
|
|
if self._resched(card):
|
|
card.due = max(self.today+1, card.odue)
|
|
else:
|
|
card.due = card.odue
|
|
card.odue = 0
|
|
else:
|
|
self._rescheduleNew(card, conf, early)
|
|
card.queue = 2
|
|
card.type = 2
|
|
# if we were dynamic, graduating means moving back to the old deck
|
|
resched = self._resched(card)
|
|
if card.odid:
|
|
card.did = card.odid
|
|
card.odue = 0
|
|
card.odid = 0
|
|
# if rescheduling is off, it needs to be set back to a new card
|
|
if not resched and not lapse:
|
|
card.queue = card.type = 0
|
|
card.due = self.col.nextID("pos")
|
|
|
|
def _startingLeft(self, card):
|
|
if card.type == 2:
|
|
conf = self._lapseConf(card)
|
|
else:
|
|
conf = self._lrnConf(card)
|
|
tot = len(conf['delays'])
|
|
tod = self._leftToday(conf['delays'], tot)
|
|
return tot + tod*1000
|
|
|
|
def _leftToday(self, delays, left, now=None):
|
|
"The number of steps that can be completed by the day cutoff."
|
|
if not now:
|
|
now = intTime()
|
|
delays = delays[-left:]
|
|
ok = 0
|
|
for i in range(len(delays)):
|
|
now += delays[i]*60
|
|
if now > self.dayCutoff:
|
|
break
|
|
ok = i
|
|
return ok+1
|
|
|
|
def _graduatingIvl(self, card, conf, early, adj=True):
|
|
if card.type == 2:
|
|
# lapsed card being relearnt
|
|
if card.odid:
|
|
if conf['resched']:
|
|
return self._dynIvlBoost(card)
|
|
return card.ivl
|
|
if not early:
|
|
# graduate
|
|
ideal = conf['ints'][0]
|
|
else:
|
|
# early remove
|
|
ideal = conf['ints'][1]
|
|
if adj:
|
|
return self._adjRevIvl(card, ideal)
|
|
else:
|
|
return ideal
|
|
|
|
def _rescheduleNew(self, card, conf, early):
|
|
"Reschedule a new card that's graduated for the first time."
|
|
card.ivl = self._graduatingIvl(card, conf, early)
|
|
card.due = self.today+card.ivl
|
|
card.factor = conf['initialFactor']
|
|
|
|
def _logLrn(self, card, ease, conf, leaving, type, lastLeft):
|
|
lastIvl = -(self._delayForGrade(conf, lastLeft))
|
|
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
|
|
def log():
|
|
self.col.db.execute(
|
|
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
|
int(time.time()*1000), card.id, self.col.usn(), ease,
|
|
ivl, lastIvl, card.factor, card.timeTaken(), type)
|
|
try:
|
|
log()
|
|
except:
|
|
# duplicate pk; retry in 10ms
|
|
time.sleep(0.01)
|
|
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 = odue, queue = 2, mod = %d, usn = %d, odue = 0
|
|
where queue in (1,3) and type = 2
|
|
%s
|
|
""" % (intTime(), self.col.usn(), extra))
|
|
# 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):
|
|
cnt = self.col.db.scalar(
|
|
"""
|
|
select sum(left/1000) from
|
|
(select left from cards where did = ? and queue = 1 and due < ? limit ?)""",
|
|
did, intTime() + self.col.conf['collapseTime'], self.reportLimit) or 0
|
|
return cnt + self.col.db.scalar(
|
|
"""
|
|
select count() from
|
|
(select 1 from cards where did = ? and queue = 3
|
|
and due <= ? limit ?)""",
|
|
did, self.today, self.reportLimit)
|
|
|
|
# Reviews
|
|
##########################################################################
|
|
|
|
def _deckRevLimit(self, did):
|
|
return self._deckNewLimit(did, self._deckRevLimitSingle)
|
|
|
|
def _deckRevLimitSingle(self, d):
|
|
if d['dyn']:
|
|
return self.reportLimit
|
|
c = self.col.decks.confForDid(d['id'])
|
|
return max(0, c['rev']['perDay'] - d['revToday'][1])
|
|
|
|
def _revForDeck(self, did, lim):
|
|
lim = min(lim, self.reportLimit)
|
|
return self.col.db.scalar(
|
|
"""
|
|
select count() from
|
|
(select 1 from cards where did = ? and queue = 2
|
|
and due <= ? limit ?)""",
|
|
did, self.today, lim)
|
|
|
|
def _resetRevCount(self):
|
|
def cntFn(did, lim):
|
|
return self.col.db.scalar("""
|
|
select count() from (select id from cards where
|
|
did = ? and queue = 2 and due <= ? limit %d)""" % lim,
|
|
did, self.today)
|
|
self.revCount = self._walkingCount(
|
|
self._deckRevLimitSingle, cntFn)
|
|
|
|
def _resetRev(self):
|
|
self._resetRevCount()
|
|
self._revQueue = []
|
|
self._revDids = self.col.decks.active()[:]
|
|
|
|
def _fillRev(self):
|
|
if self._revQueue:
|
|
return True
|
|
if not self.revCount:
|
|
return False
|
|
while self._revDids:
|
|
did = self._revDids[0]
|
|
lim = min(self.queueLimit, self._deckRevLimit(did))
|
|
if lim:
|
|
# fill the queue with the current did
|
|
self._revQueue = self.col.db.list("""
|
|
select id from cards where
|
|
did = ? and queue = 2 and due <= ? limit ?""",
|
|
did, self.today, lim)
|
|
if self._revQueue:
|
|
# ordering
|
|
if self.col.decks.get(did)['dyn']:
|
|
# dynamic decks need due order preserved
|
|
self._revQueue.reverse()
|
|
else:
|
|
# random order for regular reviews
|
|
r = random.Random()
|
|
r.seed(self.today)
|
|
r.shuffle(self._revQueue)
|
|
# is the current did empty?
|
|
if len(self._revQueue) < lim:
|
|
self._revDids.pop(0)
|
|
return True
|
|
# nothing left in the deck; move to next
|
|
self._revDids.pop(0)
|
|
|
|
def _getRevCard(self):
|
|
if self._fillRev():
|
|
self.revCount -= 1
|
|
return self.col.getCard(self._revQueue.pop())
|
|
|
|
def totalRevForCurrentDeck(self):
|
|
return self.col.db.scalar(
|
|
"""
|
|
select count() from cards where id in (
|
|
select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
|
|
% ids2str(self.col.decks.active()), self.today, self.reportLimit)
|
|
|
|
# Answering a review card
|
|
##########################################################################
|
|
|
|
def _answerRevCard(self, card, ease):
|
|
delay = 0
|
|
if ease == 1:
|
|
delay = self._rescheduleLapse(card)
|
|
else:
|
|
self._rescheduleRev(card, ease)
|
|
self._logRev(card, ease, delay)
|
|
|
|
def _rescheduleLapse(self, card):
|
|
conf = self._lapseConf(card)
|
|
card.lastIvl = card.ivl
|
|
if self._resched(card):
|
|
card.lapses += 1
|
|
card.ivl = self._nextLapseIvl(card, conf)
|
|
card.factor = max(1300, card.factor-200)
|
|
card.due = self.today + card.ivl
|
|
# if it's a filtered deck, update odue as well
|
|
if card.odid:
|
|
card.odue = card.due
|
|
# if suspended as a leech, nothing to do
|
|
delay = 0
|
|
if self._checkLeech(card, conf) and card.queue == -1:
|
|
return delay
|
|
# if no relearning steps, nothing to do
|
|
if not conf['delays']:
|
|
return delay
|
|
# record rev due date for later
|
|
if not card.odue:
|
|
card.odue = card.due
|
|
delay = self._delayForGrade(conf, 0)
|
|
card.due = int(delay + time.time())
|
|
card.left = self._startingLeft(card)
|
|
# queue 1
|
|
if card.due < self.dayCutoff:
|
|
self.lrnCount += card.left/1000
|
|
card.queue = 1
|
|
heappush(self._lrnQueue, (card.due, card.id))
|
|
else:
|
|
# day learn queue
|
|
ahead = ((card.due - self.dayCutoff) / 86400) + 1
|
|
card.due = self.today + ahead
|
|
card.queue = 3
|
|
return delay
|
|
|
|
def _nextLapseIvl(self, card, conf):
|
|
return max(conf['minInt'], int(card.ivl*conf['mult']))
|
|
|
|
def _rescheduleRev(self, card, ease):
|
|
# update interval
|
|
card.lastIvl = card.ivl
|
|
if self._resched(card):
|
|
self._updateRevIvl(card, ease)
|
|
# then the rest
|
|
card.factor = max(1300, card.factor+[-150, 0, 150][ease-2])
|
|
card.due = self.today + card.ivl
|
|
else:
|
|
card.due = card.odue
|
|
if card.odid:
|
|
card.did = card.odid
|
|
card.odid = 0
|
|
card.odue = 0
|
|
|
|
def _logRev(self, card, ease, delay):
|
|
def log():
|
|
self.col.db.execute(
|
|
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
|
int(time.time()*1000), card.id, self.col.usn(), ease,
|
|
-delay or card.ivl, card.lastIvl, card.factor, card.timeTaken(),
|
|
1)
|
|
try:
|
|
log()
|
|
except:
|
|
# duplicate pk; retry in 10ms
|
|
time.sleep(0.01)
|
|
log()
|
|
|
|
# Interval management
|
|
##########################################################################
|
|
|
|
def _nextRevIvl(self, card, ease):
|
|
"Ideal next interval for CARD, given EASE."
|
|
delay = self._daysLate(card)
|
|
conf = self._revConf(card)
|
|
fct = card.factor / 1000.0
|
|
ivl2 = self._constrainedIvl((card.ivl + delay/4) * 1.2, conf, card.ivl)
|
|
ivl3 = self._constrainedIvl((card.ivl + delay/2) * fct, conf, ivl2)
|
|
ivl4 = self._constrainedIvl(
|
|
(card.ivl + delay) * fct * conf['ease4'], conf, ivl3)
|
|
if ease == 2:
|
|
interval = ivl2
|
|
elif ease == 3:
|
|
interval = ivl3
|
|
elif ease == 4:
|
|
interval = ivl4
|
|
# interval capped?
|
|
return min(interval, conf['maxIvl'])
|
|
|
|
def _fuzzedIvl(self, ivl):
|
|
min, max = self._fuzzIvlRange(ivl)
|
|
return random.randint(min, max)
|
|
|
|
def _fuzzIvlRange(self, ivl):
|
|
if ivl < 2:
|
|
return [1, 1]
|
|
elif ivl == 2:
|
|
return [2, 3]
|
|
elif ivl < 7:
|
|
fuzz = int(ivl*0.25)
|
|
elif ivl < 30:
|
|
fuzz = max(2, int(ivl*0.15))
|
|
else:
|
|
fuzz = max(4, int(ivl*0.05))
|
|
# fuzz at least a day
|
|
fuzz = max(fuzz, 1)
|
|
return [ivl-fuzz, ivl+fuzz]
|
|
|
|
def _constrainedIvl(self, ivl, conf, prev):
|
|
"Integer interval after interval factor and prev+1 constraints applied."
|
|
new = ivl * conf.get('ivlFct', 1)
|
|
return int(max(new, prev+1))
|
|
|
|
def _daysLate(self, card):
|
|
"Number of days later than scheduled."
|
|
due = card.odue if card.odid else card.due
|
|
return max(0, self.today - due)
|
|
|
|
def _updateRevIvl(self, card, ease):
|
|
"Update CARD's interval, trying to avoid siblings."
|
|
idealIvl = self._nextRevIvl(card, ease)
|
|
card.ivl = self._adjRevIvl(card, idealIvl)
|
|
|
|
def _adjRevIvl(self, card, idealIvl):
|
|
"Given IDEALIVL, return an IVL away from siblings."
|
|
if self._spreadRev:
|
|
idealIvl = self._fuzzedIvl(idealIvl)
|
|
idealDue = self.today + idealIvl
|
|
conf = self._revConf(card)
|
|
# find sibling positions
|
|
dues = self.col.db.list(
|
|
"select due from cards where nid = ? and type = 2"
|
|
" and id != ?", card.nid, card.id)
|
|
if not dues or idealDue not in dues:
|
|
return idealIvl
|
|
else:
|
|
leeway = max(conf['minSpace'], int(idealIvl * conf['fuzz']))
|
|
fudge = 0
|
|
# do we have any room to adjust the interval?
|
|
if leeway:
|
|
# loop through possible due dates for an empty one
|
|
for diff in range(1, leeway+1):
|
|
# ensure we're due at least tomorrow
|
|
if idealIvl - diff >= 1 and (idealDue - diff) not in dues:
|
|
fudge = -diff
|
|
break
|
|
elif (idealDue + diff) not in dues:
|
|
fudge = diff
|
|
break
|
|
return idealIvl + fudge
|
|
|
|
# Dynamic deck handling
|
|
##########################################################################
|
|
|
|
def rebuildDyn(self, did=None):
|
|
"Rebuild a dynamic deck."
|
|
did = did or self.col.decks.selected()
|
|
deck = self.col.decks.get(did)
|
|
assert deck['dyn']
|
|
# move any existing cards back first, then fill
|
|
self.emptyDyn(did)
|
|
ids = self._fillDyn(deck)
|
|
if not ids:
|
|
return
|
|
# and change to our new deck
|
|
self.col.decks.select(did)
|
|
return ids
|
|
|
|
def _fillDyn(self, deck):
|
|
search, limit, order = deck['terms'][0]
|
|
orderlimit = self._dynOrder(order, limit)
|
|
search += " -is:suspended -deck:filtered"
|
|
try:
|
|
ids = self.col.findCards(search, order=orderlimit)
|
|
except:
|
|
ids = []
|
|
return ids
|
|
# move the cards over
|
|
self._moveToDyn(deck['id'], ids)
|
|
return ids
|
|
|
|
def emptyDyn(self, did, lim=None):
|
|
if not lim:
|
|
lim = "did = %s" % did
|
|
# move out of cram queue
|
|
self.col.db.execute("""
|
|
update cards set did = odid, queue = (case when type = 1 then 0
|
|
else type end), type = (case when type = 1 then 0 else type end),
|
|
due = odue, odue = 0, odid = 0, usn = ?, mod = ? where %s""" % lim,
|
|
self.col.usn(), intTime())
|
|
|
|
def remFromDyn(self, cids):
|
|
self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
|
|
|
|
def _dynOrder(self, o, l):
|
|
if o == DYN_OLDEST:
|
|
t = "c.mod"
|
|
elif o == DYN_RANDOM:
|
|
t = "random()"
|
|
elif o == DYN_SMALLINT:
|
|
t = "ivl"
|
|
elif o == DYN_BIGINT:
|
|
t = "ivl desc"
|
|
elif o == DYN_LAPSES:
|
|
t = "lapses desc"
|
|
elif o == DYN_ADDED:
|
|
t = "n.id"
|
|
elif o == DYN_REVADDED:
|
|
t = "n.id desc"
|
|
elif o == DYN_DUE:
|
|
t = "c.due"
|
|
else:
|
|
# if we don't understand the term, default to due order
|
|
t = "c.due"
|
|
return t + " limit %d" % l
|
|
|
|
def _moveToDyn(self, did, ids):
|
|
deck = self.col.decks.get(did)
|
|
data = []
|
|
t = intTime(); u = self.col.usn()
|
|
for c, id in enumerate(ids):
|
|
# start at -100000 so that reviews are all due
|
|
data.append((did, -100000+c, t, u, id))
|
|
# due reviews stay in the review queue. careful: can't use
|
|
# "odid or did", as sqlite converts to boolean
|
|
queue = """
|
|
(case when type=2 and (case when odue then odue <= %d else due <= %d end)
|
|
then 2 else 0 end)"""
|
|
queue %= (self.today, self.today)
|
|
self.col.db.executemany("""
|
|
update cards set
|
|
odid = (case when odid then odid else did end),
|
|
odue = (case when odue then odue else due end),
|
|
did = ?, queue = %s, due = ?, mod = ?, usn = ? where id = ?""" % queue, data)
|
|
|
|
def _dynIvlBoost(self, card):
|
|
assert card.odid and card.type == 2
|
|
assert card.factor
|
|
elapsed = card.ivl - (card.odue - self.today)
|
|
factor = ((card.factor/1000.0)+1.2)/2.0
|
|
ivl = int(max(card.ivl, elapsed * factor, 1))
|
|
conf = self._revConf(card)
|
|
return min(conf['maxIvl'], ivl)
|
|
|
|
# Leeches
|
|
##########################################################################
|
|
|
|
def _checkLeech(self, card, conf):
|
|
"Leech handler. True if card was a leech."
|
|
lf = conf['leechFails']
|
|
if not lf:
|
|
return
|
|
# if over threshold or every half threshold reps after that
|
|
if (card.lapses >= lf and
|
|
(card.lapses-lf) % (max(lf/2, 1)) == 0):
|
|
# add a leech tag
|
|
f = card.note()
|
|
f.addTag("leech")
|
|
f.flush()
|
|
# handle
|
|
a = conf['leechAction']
|
|
if a == 0:
|
|
# if it has an old due, remove it from cram/relearning
|
|
if card.odue:
|
|
card.due = card.odue
|
|
if card.odid:
|
|
card.did = card.odid
|
|
card.odue = card.odid = 0
|
|
card.queue = -1
|
|
# notify UI
|
|
runHook("leech", card)
|
|
return True
|
|
|
|
# Tools
|
|
##########################################################################
|
|
|
|
def _cardConf(self, card):
|
|
return self.col.decks.confForDid(card.did)
|
|
|
|
def _newConf(self, card):
|
|
conf = self._cardConf(card)
|
|
# normal deck
|
|
if not card.odid:
|
|
return conf['new']
|
|
# dynamic deck; override some attributes, use original deck for others
|
|
oconf = self.col.decks.confForDid(card.odid)
|
|
delays = conf['delays'] or oconf['new']['delays']
|
|
return dict(
|
|
# original deck
|
|
ints=oconf['new']['ints'],
|
|
initialFactor=oconf['new']['initialFactor'],
|
|
# overrides
|
|
delays=delays,
|
|
separate=conf['separate'],
|
|
order=NEW_CARDS_DUE,
|
|
perDay=self.reportLimit
|
|
)
|
|
|
|
def _lapseConf(self, card):
|
|
conf = self._cardConf(card)
|
|
# normal deck
|
|
if not card.odid:
|
|
return conf['lapse']
|
|
# dynamic deck; override some attributes, use original deck for others
|
|
oconf = self.col.decks.confForDid(card.odid)
|
|
delays = conf['delays'] or oconf['lapse']['delays']
|
|
return dict(
|
|
# original deck
|
|
minInt=oconf['lapse']['minInt'],
|
|
leechFails=oconf['lapse']['leechFails'],
|
|
leechAction=oconf['lapse']['leechAction'],
|
|
mult=oconf['lapse']['mult'],
|
|
# overrides
|
|
delays=delays,
|
|
resched=conf['resched'],
|
|
)
|
|
|
|
def _revConf(self, card):
|
|
conf = self._cardConf(card)
|
|
# normal deck
|
|
if not card.odid:
|
|
return conf['rev']
|
|
# dynamic deck
|
|
return self.col.decks.confForDid(card.odid)['rev']
|
|
|
|
def _deckLimit(self):
|
|
return ids2str(self.col.decks.active())
|
|
|
|
def _resched(self, card):
|
|
conf = self._cardConf(card)
|
|
if not conf['dyn']:
|
|
return True
|
|
return conf['resched']
|
|
|
|
# Daily cutoff
|
|
##########################################################################
|
|
|
|
def _updateCutoff(self):
|
|
# days since col created
|
|
self.today = int((time.time() - self.col.crt) / 86400)
|
|
# end of day cutoff
|
|
self.dayCutoff = self.col.crt + (self.today+1)*86400
|
|
# update all daily counts, but don't save decks to prevent needless
|
|
# conflicts. we'll save on card answer instead
|
|
def update(g):
|
|
for t in "new", "rev", "lrn", "time":
|
|
key = t+"Today"
|
|
if g[key][0] != self.today:
|
|
g[key] = [self.today, 0]
|
|
for deck in self.col.decks.all():
|
|
update(deck)
|
|
|
|
def _checkDay(self):
|
|
# check if the day has rolled over
|
|
if time.time() > self.dayCutoff:
|
|
self.reset()
|
|
|
|
# Deck finished state
|
|
##########################################################################
|
|
|
|
def finishedMsg(self):
|
|
return ("<b>"+_(
|
|
"Congratulations! You have finished this deck for now.")+
|
|
"</b><br><br>" + self._nextDueMsg())
|
|
|
|
def _nextDueMsg(self):
|
|
line = []
|
|
if self.revDue():
|
|
line.append(_("""\
|
|
Today's review limit has been reached, but there are still cards
|
|
waiting to be reviewed. For optimum memory, consider increasing
|
|
the daily limit in the options."""))
|
|
if self.newDue():
|
|
line.append(_("""\
|
|
There are more new cards available, but the daily limit has been
|
|
reached. You can increase the limit in the options, but please
|
|
bear in mind that the more new cards you introduce, the higher
|
|
your short-term review workload will become."""))
|
|
if self.haveCustomStudy and not self.col.decks.current()['dyn']:
|
|
line.append(_("""\
|
|
To study outside of the normal schedule, click the Custom Study button below."""))
|
|
return "<p>".join(line)
|
|
|
|
def revDue(self):
|
|
"True if there are any rev cards due."
|
|
return self.col.db.scalar(
|
|
("select 1 from cards where did in %s and queue = 2 "
|
|
"and due <= ? limit 1") % self._deckLimit(),
|
|
self.today)
|
|
|
|
def newDue(self):
|
|
"True if there are any new cards due."
|
|
return self.col.db.scalar(
|
|
("select 1 from cards where did in %s and queue = 0 "
|
|
"limit 1") % self._deckLimit())
|
|
|
|
# Next time reports
|
|
##########################################################################
|
|
|
|
def nextIvlStr(self, card, ease, short=False):
|
|
"Return the next interval for CARD as a string."
|
|
ivl = self.nextIvl(card, ease)
|
|
if not ivl:
|
|
return ""
|
|
s = fmtTimeSpan(ivl, short=short)
|
|
if ivl < self.col.conf['collapseTime']:
|
|
s = "<"+s
|
|
return s
|
|
|
|
def nextIvl(self, card, ease):
|
|
"Return the next interval for CARD, in seconds."
|
|
if card.queue in (0,1,3):
|
|
return self._nextLrnIvl(card, ease)
|
|
elif ease == 1:
|
|
# lapsed
|
|
conf = self._lapseConf(card)
|
|
if conf['delays']:
|
|
return conf['delays'][0]*60
|
|
return self._nextLapseIvl(card, conf)*86400
|
|
else:
|
|
# review
|
|
return self._nextRevIvl(card, ease)*86400
|
|
|
|
# this isn't easily extracted from the learn code
|
|
def _nextLrnIvl(self, card, ease):
|
|
if card.queue == 0:
|
|
card.left = self._startingLeft(card)
|
|
conf = self._lrnConf(card)
|
|
if ease == 1:
|
|
# fail
|
|
return self._delayForGrade(conf, len(conf['delays']))
|
|
elif ease == 3:
|
|
# early removal
|
|
if not self._resched(card):
|
|
return 0
|
|
return self._graduatingIvl(card, conf, True, adj=False) * 86400
|
|
else:
|
|
left = card.left%1000 - 1
|
|
if left <= 0:
|
|
# graduate
|
|
if not self._resched(card):
|
|
return 0
|
|
return self._graduatingIvl(card, conf, False, adj=False) * 86400
|
|
else:
|
|
return self._delayForGrade(conf, left)
|
|
|
|
# Suspending
|
|
##########################################################################
|
|
|
|
def suspendCards(self, ids):
|
|
"Suspend cards."
|
|
self.remFromDyn(ids)
|
|
self.removeLrn(ids)
|
|
self.col.db.execute(
|
|
"update cards set queue=-1,mod=?,usn=? where id in "+
|
|
ids2str(ids), intTime(), self.col.usn())
|
|
|
|
def unsuspendCards(self, ids):
|
|
"Unsuspend cards."
|
|
self.col.db.execute(
|
|
"update cards set queue=type,mod=?,usn=? "
|
|
"where queue = -1 and id in "+ ids2str(ids),
|
|
intTime(), self.col.usn())
|
|
|
|
def buryNote(self, nid):
|
|
"Bury all cards for note until next session."
|
|
self.col.setDirty()
|
|
cids = self.col.db.list(
|
|
"select id from cards where nid = ? and queue >= 0", nid)
|
|
self.removeLrn(cids)
|
|
self.col.db.execute("update cards set queue = -2 where id in "+ids2str(cids))
|
|
|
|
# Resetting
|
|
##########################################################################
|
|
|
|
def forgetCards(self, ids):
|
|
"Put cards at the end of the new queue."
|
|
self.col.db.execute(
|
|
"update cards set type=0,queue=0,ivl=0,factor=? where id in "+
|
|
ids2str(ids), 2500)
|
|
pmax = self.col.db.scalar(
|
|
"select max(due) from cards where type=0") or 0
|
|
# takes care of mod + usn
|
|
self.sortCards(ids, start=pmax+1)
|
|
|
|
def reschedCards(self, ids, imin, imax):
|
|
"Put cards in review queue with a new interval in days (min, max)."
|
|
d = []
|
|
t = self.today
|
|
mod = intTime()
|
|
for id in ids:
|
|
r = random.randint(imin, imax)
|
|
d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod,
|
|
usn=self.col.usn(), fact=2500))
|
|
self.col.db.executemany("""
|
|
update cards set type=2,queue=2,ivl=:ivl,due=:due,
|
|
usn=:usn, mod=:mod, factor=:fact where id=:id and odid=0""",
|
|
d)
|
|
|
|
def resetCards(self, ids):
|
|
"Completely reset cards for export."
|
|
self.col.db.execute(
|
|
"update cards set reps=0, lapses=0 where id in " + ids2str(ids))
|
|
self.forgetCards(ids)
|
|
|
|
# Repositioning new cards
|
|
##########################################################################
|
|
|
|
def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False):
|
|
scids = ids2str(cids)
|
|
now = intTime()
|
|
nids = []
|
|
nidsSet = set()
|
|
for id in cids:
|
|
nid = self.col.db.scalar("select nid from cards where id = ?", id)
|
|
if nid not in nidsSet:
|
|
nids.append(nid)
|
|
nidsSet.add(nid)
|
|
if not nids:
|
|
# no new cards
|
|
return
|
|
# determine nid ordering
|
|
due = {}
|
|
if shuffle:
|
|
random.shuffle(nids)
|
|
for c, nid in enumerate(nids):
|
|
due[nid] = start+c*step
|
|
high = start+c*step
|
|
# shift?
|
|
if shift:
|
|
low = self.col.db.scalar(
|
|
"select min(due) from cards where due >= ? and type = 0 "
|
|
"and id not in %s" % scids,
|
|
start)
|
|
if low is not None:
|
|
shiftby = high - low + 1
|
|
self.col.db.execute("""
|
|
update cards set mod=?, usn=?, due=due+? where id not in %s
|
|
and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low)
|
|
# reorder cards
|
|
d = []
|
|
for id, nid in self.col.db.execute(
|
|
"select id, nid from cards where type = 0 and id in "+scids):
|
|
d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id))
|
|
self.col.db.executemany(
|
|
"update cards set due=:due,mod=:now,usn=:usn where id = :cid", d)
|
|
|
|
def randomizeCards(self, did):
|
|
cids = self.col.db.list("select id from cards where did = ?", did)
|
|
self.sortCards(cids, shuffle=True)
|
|
|
|
def orderCards(self, did):
|
|
cids = self.col.db.list("select id from cards where did = ? order by id", did)
|
|
self.sortCards(cids)
|
|
|
|
def resortConf(self, conf):
|
|
for did in self.col.decks.didsForConf(conf):
|
|
if conf['new']['order'] == 0:
|
|
self.randomizeCards(did)
|
|
else:
|
|
self.orderCards(did)
|