moving scheduling code into separate file, some preliminary refactoring

This commit is contained in:
Damien Elmes 2011-02-26 14:37:49 +09:00
parent 8cbd3c9f3b
commit 4e7e8b03bc
10 changed files with 784 additions and 855 deletions

View file

@ -14,8 +14,16 @@ MAX_TIMER = 60
# Cards
##########################################################################
# Type: 0=lapsed, 1=due, 2=new, 3=drilled
# Queue: under normal circumstances, same as type.
# tasks:
# - remove all failed cards from learning queue - set queue=1; type=1 and
# leave scheduling parameters alone (need separate due for learn queue and
# reviews)
#
# - cram cards. gather and introduce to queue=0.
# - remove all cram cards from learning queue. if type h
# Type: 0=new+learning, 1=due, 2=new, 3=failed+learning, 4=cram+learning
# Queue: 0=learning, 1=due, 2=new, 3=new today,
# -1=suspended, -2=user buried, -3=sched buried (rev early, etc)
# Ordinal: card template # for fact
# Position: sorting position, only for new cards

View file

@ -22,6 +22,7 @@ from anki.template import render
from anki.media import updateMediaCount, mediaFiles, \
rebuildMediaDir
from anki.upgrade import upgradeSchema, updateIndices, upgradeDeck, DECK_VERSION
from anki.sched import Scheduler
import anki.latex # sets up hook
# ensure all the DB metadata in other files is loaded before proceeding
@ -107,7 +108,6 @@ class Deck(object):
self.sessionStartReps = 0
self.sessionStartTime = 0
self.lastSessionStart = 0
self.queueLimit = 200
# if most recent deck var not defined, make sure defaults are set
if not self.db.scalar("select 1 from deckVars where key = 'latexPost'"):
self.setVarDefault("mediaURL", "")
@ -121,821 +121,28 @@ class Deck(object):
\\begin{document}
""")
self.setVarDefault("latexPost", "\\end{document}")
self.updateCutoff()
self.setupStandardScheduler()
self.sched = Scheduler(self)
def modifiedSinceSave(self):
return self.modified > self.lastLoaded
# Queue management
##########################################################################
def setupStandardScheduler(self):
self.getCardId = self._getCardId
self.fillFailedQueue = self._fillFailedQueue
self.fillRevQueue = self._fillRevQueue
self.fillNewQueue = self._fillNewQueue
self.rebuildFailedCount = self._rebuildFailedCount
self.rebuildRevCount = self._rebuildRevCount
self.rebuildNewCount = self._rebuildNewCount
self.requeueCard = self._requeueCard
self.timeForNewCard = self._timeForNewCard
self.updateNewCountToday = self._updateNewCountToday
self.cardQueue = self._cardQueue
self.finishScheduler = None
self.answerCard = self._answerCard
self.cardLimit = self._cardLimit
self.answerPreSave = None
self.spaceCards = self._spaceCards
self.scheduler = "standard"
# restore any cards temporarily suspended by alternate schedulers
try:
self.resetAfterReviewEarly()
except OperationalError, e:
# will fail if deck hasn't been upgraded yet
pass
def fillQueues(self):
self.fillFailedQueue()
self.fillRevQueue()
self.fillNewQueue()
def rebuildCounts(self):
# global counts
self.cardCount = self.db.scalar("select count(*) from cards")
self.factCount = self.db.scalar("select count(*) from facts")
# day counts
(self.repsToday, self.newSeenToday) = self.db.first("""
select count(), sum(case when rep = 1 then 1 else 0 end) from revlog
where time > :t""", t=self.failedCutoff-86400)
self.newSeenToday = self.newSeenToday or 0
print "newSeenToday in answer(), reset called twice"
print "newSeenToday needs to account for drill mode too."
# due counts
self.rebuildFailedCount()
self.rebuildRevCount()
self.rebuildNewCount()
def _cardLimit(self, active, inactive, sql):
yes = parseTags(getattr(self, active))
no = parseTags(getattr(self, inactive))
if yes:
yids = tagIds(self.db, yes).values()
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and +c.id not in (select cardId from "
"cardTags where tagId in %s) and" % (
ids2str(yids),
ids2str(nids)))
elif no:
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id not in (select cardId from cardTags where "
"tagId in %s) and" % ids2str(nids))
else:
return sql
def _rebuildFailedCount(self):
# This is a count of all failed cards within the current day cutoff.
# The cards may not be ready for review yet, but can still be
# displayed if failedCardsMax is reached.
self.failedSoonCount = self.db.scalar(
self.cardLimit(
"revActive", "revInactive",
"select count(*) from cards c where queue = 0 "
"and due < :lim"), lim=self.failedCutoff)
def _rebuildRevCount(self):
self.revCount = self.db.scalar(
self.cardLimit(
"revActive", "revInactive",
"select count(*) from cards c where queue = 1 "
"and due < :lim"), lim=self.dueCutoff)
def _rebuildNewCount(self):
self.newAvail = self.db.scalar(
self.cardLimit(
"newActive", "newInactive",
"select count(*) from cards c where queue = 2 "
"and due < :lim"), lim=self.dueCutoff)
self.updateNewCountToday()
self.spacedCards = []
def _updateNewCountToday(self):
self.newCount = max(min(
self.newAvail, self.newCardsPerDay -
self.newSeenToday), 0)
def _fillFailedQueue(self):
if self.failedSoonCount and not self.failedQueue:
self.failedQueue = self.db.all(
self.cardLimit(
"revActive", "revInactive", """
select c.id, factId, due from cards c where
queue = 0 and due < :lim order by due
limit %d""" % self.queueLimit), lim=self.failedCutoff)
self.failedQueue.reverse()
def _fillRevQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.db.all(
self.cardLimit(
"revActive", "revInactive", """
select c.id, factId from cards c where
queue = 1 and due < :lim order by %s
limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dueCutoff)
self.revQueue.reverse()
def _fillNewQueue(self):
if self.newCount and not self.newQueue and not self.spacedCards:
self.newQueue = self.db.all(
self.cardLimit(
"newActive", "newInactive", """
select c.id, factId from cards c where
queue = 2 and due < :lim order by %s
limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dueCutoff)
self.newQueue.reverse()
def queueNotEmpty(self, queue, fillFunc, new=False):
while True:
self.removeSpaced(queue, new)
if queue:
return True
fillFunc()
if not queue:
return False
def removeSpaced(self, queue, new=False):
popped = []
delay = None
while queue:
fid = queue[-1][1]
if fid in self.spacedFacts:
# still spaced
id = queue.pop()[0]
# assuming 10 cards/minute, track id if likely to expire
# before queue refilled
if new and self.newSpacing < self.queueLimit * 6:
popped.append(id)
delay = self.spacedFacts[fid]
else:
if popped:
self.spacedCards.append((delay, popped))
return
def revNoSpaced(self):
return self.queueNotEmpty(self.revQueue, self.fillRevQueue)
def newNoSpaced(self):
return self.queueNotEmpty(self.newQueue, self.fillNewQueue, True)
def _requeueCard(self, card, oldSuc):
newType = None
try:
if card.reps == 1:
if self.newFromCache:
# fetched from spaced cache
newType = 2
cards = self.spacedCards.pop(0)[1]
# reschedule the siblings
if len(cards) > 1:
self.spacedCards.append(
(time.time() + self.newSpacing, cards[1:]))
else:
# fetched from normal queue
newType = 1
self.newQueue.pop()
elif oldSuc == 0:
self.failedQueue.pop()
else:
self.revQueue.pop()
except:
raise Exception("""\
requeueCard() failed. Please report this along with the steps you take to
produce the problem.
Counts %d %d %d
Queue %d %d %d
Card info: %d %d %d
New type: %s""" % (self.failedSoonCount, self.revCount, self.newCount,
len(self.failedQueue), len(self.revQueue),
len(self.newQueue),
card.reps, card.successive, oldSuc, `newType`))
def revOrder(self):
return ("interval desc",
"interval",
"due",
"factId, ordinal")[self.revCardOrder]
def newOrder(self):
return ("due",
"due",
"due desc")[self.newCardOrder]
def rebuildTypes(self):
"Rebuild the type cache. Only necessary on upgrade."
# set type first
self.db.statement("""
update cards set type = (case
when successive then 1 when reps then 0 else 2 end)
""")
# then queue
self.db.statement("""
update cards set queue = type
when queue != -1""")
def updateAllFieldChecksums(self):
# zero out
self.db.statement("update fields set chksum = ''")
# add back for unique fields
for m in self.models:
for fm in m.fieldModels:
self.updateFieldChecksums(fm.id)
def updateFieldChecksums(self, fmid):
self.db.flush()
self.setSchemaModified()
unique = self.db.scalar(
"select \"unique\" from fieldModels where id = :id", id=fmid)
if unique:
l = []
for (id, value) in self.db.all(
"select id, value from fields where fieldModelId = :id",
id=fmid):
l.append({'id':id, 'chk':fieldChecksum(value)})
self.db.statements(
"update fields set chksum = :chk where id = :id", l)
else:
self.db.statement(
"update fields set chksum = '' where fieldModelId=:id",
id=fmid)
def _cardQueue(self, card):
return self.cardType(card)
def cardType(self, card):
"Return the type of the current card (what queue it's in)"
if card.successive:
return 1
elif card.reps:
return 0
else:
return 2
def updateCutoff(self):
d = datetime.datetime.utcfromtimestamp(
time.time() - self.utcOffset) + datetime.timedelta(days=1)
d = datetime.datetime(d.year, d.month, d.day)
newday = self.utcOffset - time.timezone
d += datetime.timedelta(seconds=newday)
cutoff = time.mktime(d.timetuple())
# cutoff must not be in the past
while cutoff < time.time():
cutoff += 86400
# cutoff must not be more than 24 hours in the future
cutoff = min(time.time() + 86400, cutoff)
self.failedCutoff = cutoff
if self.getBool("perDay"):
self.dueCutoff = cutoff
else:
self.dueCutoff = time.time()
def reset(self):
# recheck counts
self.rebuildCounts()
# empty queues; will be refilled by getCard()
self.failedQueue = []
self.revQueue = []
self.newQueue = []
self.spacedFacts = {}
# determine new card distribution
if self.newCardSpacing == 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)
else:
self.newCardModulus = 0
else:
self.newCardModulus = 0
self.sched.reset()
# recache css
self.rebuildCSS()
# spacing for delayed cards - not to be confused with newCardSpacing
# above
print "newSpacing/revSpacing"
self.newSpacing = 0
self.revSpacing = 0
def checkDay(self):
# check if the day has rolled over
if time.time() > self.failedCutoff:
self.updateCutoff()
self.reset()
# Review early
##########################################################################
def setupReviewEarlyScheduler(self):
self.fillRevQueue = self._fillRevEarlyQueue
self.rebuildRevCount = self._rebuildRevEarlyCount
self.finishScheduler = self._onReviewEarlyFinished
self.answerPreSave = self._reviewEarlyPreSave
self.scheduler = "reviewEarly"
def _reviewEarlyPreSave(self, card, ease):
if ease > 1:
# prevent it from appearing in next queue fill
card.queue = -3
def resetAfterReviewEarly(self):
"Put temporarily suspended cards back into play. Caller must .reset()"
self.db.statement(
"update cards set queue = type where queue = -3")
def _onReviewEarlyFinished(self):
# clean up buried cards
self.resetAfterReviewEarly()
# and go back to regular scheduler
self.setupStandardScheduler()
def _rebuildRevEarlyCount(self):
# in the future it would be nice to skip the first x days of due cards
self.revCount = self.db.scalar(
self.cardLimit(
"revActive", "revInactive", """
select count() from cards c where queue = 1 and due > :now
"""), now=self.dueCutoff)
def _fillRevEarlyQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.db.all(
self.cardLimit(
"revActive", "revInactive", """
select id, factId from cards c where queue = 1 and due > :lim
order by due limit %d""" % self.queueLimit), lim=self.dueCutoff)
self.revQueue.reverse()
# Learn more
##########################################################################
def setupLearnMoreScheduler(self):
self.rebuildNewCount = self._rebuildLearnMoreCount
self.updateNewCountToday = self._updateLearnMoreCountToday
self.finishScheduler = self.setupStandardScheduler
self.scheduler = "learnMore"
def _rebuildLearnMoreCount(self):
self.newAvail = self.db.scalar(
self.cardLimit(
"newActive", "newInactive",
"select count(*) from cards c where queue = 2 "
"and due < :lim"), lim=self.dueCutoff)
self.spacedCards = []
def _updateLearnMoreCountToday(self):
self.newCount = self.newAvail
# Cramming
##########################################################################
def setupCramScheduler(self, active, order):
self.getCardId = self._getCramCardId
self.activeCramTags = active
self.cramOrder = order
self.rebuildNewCount = self._rebuildCramNewCount
self.rebuildRevCount = self._rebuildCramCount
self.rebuildFailedCount = self._rebuildFailedCramCount
self.fillRevQueue = self._fillCramQueue
self.fillFailedQueue = self._fillFailedCramQueue
self.finishScheduler = self.setupStandardScheduler
self.failedCramQueue = []
self.requeueCard = self._requeueCramCard
self.cardQueue = self._cramCardQueue
self.answerCard = self._answerCramCard
self.spaceCards = self._spaceCramCards
# reuse review early's code
self.answerPreSave = self._cramPreSave
self.cardLimit = self._cramCardLimit
self.scheduler = "cram"
def _cramPreSave(self, card, ease):
# prevent it from appearing in next queue fill
card.lastInterval = self.cramLastInterval
card.type = -3
def _spaceCramCards(self, card):
self.spacedFacts[card.factId] = time.time() + self.newSpacing
def _answerCramCard(self, card, ease):
self.cramLastInterval = card.lastInterval
self._answerCard(card, ease)
if ease == 1:
self.failedCramQueue.insert(0, [card.id, card.factId])
def _getCramCardId(self, check=True):
self.checkDay()
self.fillQueues()
if self.failedCardMax and self.failedSoonCount >= self.failedCardMax:
return self.failedQueue[-1][0]
# card due for review?
if self.revNoSpaced():
return self.revQueue[-1][0]
if self.failedQueue:
return self.failedQueue[-1][0]
if check:
# collapse spaced cards before reverting back to old scheduler
self.reset()
return self.getCardId(False)
# if we're in a custom scheduler, we may need to switch back
if self.finishScheduler:
self.finishScheduler()
self.reset()
return self.getCardId()
def _cramCardQueue(self, card):
if self.revQueue and self.revQueue[-1][0] == card.id:
return 1
else:
return 0
def _requeueCramCard(self, card, oldSuc):
if self.cardQueue(card) == 1:
self.revQueue.pop()
else:
self.failedCramQueue.pop()
def _rebuildCramNewCount(self):
self.newAvail = 0
self.newCount = 0
def _cramCardLimit(self, active, inactive, sql):
# inactive is (currently) ignored
if isinstance(active, list):
return sql.replace(
"where", "where +c.id in " + ids2str(active) + " and")
else:
yes = parseTags(active)
if yes:
yids = tagIds(self.db, yes).values()
return sql.replace(
"where ",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and " % ids2str(yids))
else:
return sql
def _fillCramQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.db.all(self.cardLimit(
self.activeCramTags, "", """
select id, factId from cards c
where queue between 0 and 2
order by %s
limit %s""" % (self.cramOrder, self.queueLimit)))
self.revQueue.reverse()
def _rebuildCramCount(self):
self.revCount = self.db.scalar(self.cardLimit(
self.activeCramTags, "",
"select count(*) from cards c where queue between 0 and 2"))
def _rebuildFailedCramCount(self):
self.failedSoonCount = len(self.failedCramQueue)
def _fillFailedCramQueue(self):
self.failedQueue = self.failedCramQueue
# Getting the next card
##########################################################################
def getCard(self, orm=True):
"Return the next card object, or None."
id = self.getCardId()
if id:
return self.cardFromId(id, orm)
else:
self.stopSession()
def _getCardId(self, check=True):
"Return the next due card id, or None."
self.checkDay()
self.fillQueues()
self.updateNewCountToday()
if self.failedQueue:
# failed card due?
if self.delay0:
if self.failedQueue[-1][2] + self.delay0 < time.time():
return self.failedQueue[-1][0]
# failed card queue too big?
if (self.failedCardMax and
self.failedSoonCount >= self.failedCardMax):
return self.failedQueue[-1][0]
# distribute new cards?
if self.newNoSpaced() and self.timeForNewCard():
return self.getNewCard()
# card due for review?
if self.revNoSpaced():
return self.revQueue[-1][0]
# new cards left?
if self.newCount:
id = self.getNewCard()
if id:
return id
if check:
# check for expired cards, or new day rollover
self.updateCutoff()
self.reset()
return self.getCardId(check=False)
# display failed cards early/last
if not check and self.showFailedLast() and self.failedQueue:
return self.failedQueue[-1][0]
# if we're in a custom scheduler, we may need to switch back
if self.finishScheduler:
self.finishScheduler()
self.reset()
return self.getCardId()
# Get card: helper functions
##########################################################################
def _timeForNewCard(self):
"True if it's time to display a new card when distributing."
if not self.newCount:
return False
if self.newCardSpacing == NEW_CARDS_LAST:
return False
if self.newCardSpacing == NEW_CARDS_FIRST:
return True
if self.newCardModulus:
return self.repsToday % self.newCardModulus == 0
else:
return False
def getNewCard(self):
src = None
if (self.spacedCards and
self.spacedCards[0][0] < time.time()):
# spaced card has expired
src = 0
elif self.newQueue:
# card left in new queue
src = 1
elif self.spacedCards:
# card left in spaced queue
src = 0
else:
# only cards spaced to another day left
return
if src == 0:
cards = self.spacedCards[0][1]
self.newFromCache = True
return cards[0]
else:
self.newFromCache = False
return self.newQueue[-1][0]
def showFailedLast(self):
return self.collapseTime or not self.delay0
def cardFromId(self, id, orm=False):
"Given a card ID, return a card, and start the card timer."
if orm:
card = self.db.query(anki.cards.Card).get(id)
if not card:
return
card.timerStopped = False
else:
card = anki.cards.Card()
if not card.fromDB(self.db, id):
return
card.deck = self
#card.genFuzz()
card.startTimer()
return card
# Answering a card
##########################################################################
def _answerCard(self, card, ease):
undoName = _("Answer Card")
self.setUndoStart(undoName)
now = time.time()
# old state
oldState = self.cardState(card)
oldQueue = self.cardQueue(card)
lastDelaySecs = time.time() - card.due
lastDelay = lastDelaySecs / 86400.0
oldSuc = card.successive
# update card details
last = card.interval
card.interval = self.nextInterval(card, ease)
card.lastInterval = last
if card.reps:
# only update if card was not new
card.lastDue = card.due
card.due = self.nextDue(card, ease, oldState)
if not self.finishScheduler:
# don't update factor in custom schedulers
self.updateFactor(card, ease)
# spacing
self.spaceCards(card)
# adjust counts for current card
if ease == 1:
if card.due < self.failedCutoff:
self.failedSoonCount += 1
if oldQueue == 0:
self.failedSoonCount -= 1
elif oldQueue == 1:
self.revCount -= 1
else:
self.newAvail -= 1
# card stats
self.updateCardStats(card, ease, oldState)
# update type & ensure past cutoff
card.type = self.cardType(card)
card.queue = card.type
if ease != 1:
card.due = max(card.due, self.dueCutoff+1)
# allow custom schedulers to munge the card
if self.answerPreSave:
self.answerPreSave(card, ease)
# save
card.due = card.due
card.toDB(self.db)
# review history
print "make sure flags is set correctly when reviewing early"
logReview(self.db, card, ease, 0)
self.modified = now
# remove from queue
self.requeueCard(card, oldSuc)
# leech handling - we need to do this after the queue, as it may cause
# a reset()
isLeech = self.isLeech(card)
if isLeech:
self.handleLeech(card)
runHook("cardAnswered", card.id, isLeech)
self.setUndoEnd(undoName)
def updateCardStats(self, card, ease, state):
card.reps += 1
if ease == 1:
card.successive = 0
card.lapses += 1
else:
card.successive += 1
# if not card.firstAnswered:
# card.firstAnswered = time.time()
card.setModified()
def _spaceCards(self, card):
new = time.time() + self.newSpacing
self.db.statement("""
update cards set
due = (case
when queue = 1 then due + 86400 * (case
when interval*:rev < 1 then 0
else interval*:rev
end)
when queue = 2 then :new
end),
modified = :now
where id != :id and factId = :factId
and due < :cut
and queue between 1 and 2""",
id=card.id, now=time.time(), factId=card.factId,
cut=self.dueCutoff, new=new, rev=self.revSpacing)
# update local cache of seen facts
self.spacedFacts[card.factId] = new
def isLeech(self, card):
no = card.lapses
fmax = self.getInt('leechFails')
if not fmax:
return
return (
# failed
not card.successive and
# greater than fail threshold
no >= fmax and
# at least threshold/2 reps since last time
(fmax - no) % (max(fmax/2, 1)) == 0)
def handleLeech(self, card):
self.refreshSession()
scard = self.cardFromId(card.id, True)
tags = scard.fact.tags
tags = addTags("Leech", tags)
scard.fact.tags = canonifyTags(tags)
scard.fact.setModified(textChanged=True, deck=self)
self.updateFactTags([scard.fact.id])
self.db.flush()
self.db.expunge(scard)
if self.getBool('suspendLeeches'):
self.suspendCards([card.id])
self.reset()
self.refreshSession()
# Interval management
##########################################################################
def nextInterval(self, card, ease):
"Return the next interval for CARD given EASE."
delay = self._adjustedDelay(card, ease)
return self._nextInterval(card, delay, ease)
def _nextInterval(self, card, delay, ease):
interval = card.interval
factor = card.factor
# if cramming / reviewing early
if delay < 0:
interval = max(card.lastInterval, card.interval + delay)
if interval < self.midIntervalMin:
interval = 0
delay = 0
# if interval is less than mid interval, use presets
if ease == 1:
interval *= self.delay2
if interval < self.hardIntervalMin:
interval = 0
elif interval == 0:
if ease == 2:
interval = random.uniform(self.hardIntervalMin,
self.hardIntervalMax)
elif ease == 3:
interval = random.uniform(self.midIntervalMin,
self.midIntervalMax)
elif ease == 4:
interval = random.uniform(self.easyIntervalMin,
self.easyIntervalMax)
else:
# if not cramming, boost initial 2
if (interval < self.hardIntervalMax and
interval > 0.166):
mid = (self.midIntervalMin + self.midIntervalMax) / 2.0
interval = mid / factor
# multiply last interval by factor
if ease == 2:
interval = (interval + delay/4) * 1.2
elif ease == 3:
interval = (interval + delay/2) * factor
elif ease == 4:
interval = (interval + delay) * factor * self.factorFour
fuzz = random.uniform(0.95, 1.05)
interval *= fuzz
return interval
def nextIntervalStr(self, card, ease, short=False):
"Return the next interval for CARD given EASE as a string."
int = self.nextInterval(card, ease)
return anki.utils.fmtTimeSpan(int*86400, short=short)
def nextDue(self, card, ease, oldState):
"Return time when CARD will expire given EASE."
if ease == 1:
# 600 is a magic value which means no bonus, and is used to ease
# upgrades
cram = self.scheduler == "cram"
if (not cram and oldState == "mature"
and self.delay1 and self.delay1 != 600):
# user wants a bonus of 1+ days. put the failed cards at the
# start of the future day, so that failures that day will come
# after the waiting cards
return self.failedCutoff + (self.delay1 - 1)*86400
else:
due = 0
else:
due = card.interval * 86400.0
return due + time.time()
def updateFactor(self, card, ease):
"Update CARD's factor based on EASE."
print "update cardIsBeingLearnt()"
if not card.reps:
# card is new, inherit beginning factor
card.factor = self.averageFactor
if card.successive and not self.cardIsBeingLearnt(card):
if ease == 1:
card.factor -= 0.20
elif ease == 2:
card.factor -= 0.15
if ease == 4:
card.factor += 0.10
card.factor = max(1.3, card.factor)
def _adjustedDelay(self, card, ease):
"Return an adjusted delay value for CARD based on EASE."
if self.cardIsNew(card):
return 0
if card.due <= self.dueCutoff:
return (self.dueCutoff - card.due) / 86400.0
else:
return (self.dueCutoff - card.due) / 86400.0
def getCard(self):
return self.sched.getCard()
# if card:
# return card
# if sched.name == "main":
# self.stopSession()
# else:
# # in a custom scheduler; return to normal
# print "fixme: this should be done in gui code"
# self.sched.cleanup()
# self.sched = AnkiScheduler(self)
# return self.getCard()
def resetCards(self, ids=None):
"Reset progress on cards in IDS."
@ -1015,9 +222,9 @@ where id = :id""", vals)
next = self.earliestTime()
if next:
# all new cards except suspended
newCount = self.newCardsDueBy(self.dueCutoff + 86400)
newCount = self.newCardsDueBy(self.dayCutoff + 86400)
newCardsTomorrow = min(newCount, self.newCardsPerDay)
cards = self.cardsDueBy(self.dueCutoff + 86400)
cards = self.cardsDueBy(self.dayCutoff + 86400)
msg = _('''\
<style>b { color: #00f; }</style>
At this time tomorrow:<br>
@ -1029,7 +236,7 @@ At this time tomorrow:<br>
'wait': ngettext("There will be <b>%s review</b>.",
"There will be <b>%s reviews</b>.", cards) % cards,
}
if next > (self.dueCutoff+86400) and not newCardsTomorrow:
if next > (self.dayCutoff+86400) and not newCardsTomorrow:
msg = (_("The next review is in <b>%s</b>.") %
self.earliestTimeStr())
else:
@ -1143,7 +350,7 @@ where queue = -1 and id in %s""" %
"Assumes queue finished. True if some due cards have not been shown."
return self.db.scalar("""
select 1 from cards where due < :now
and queue between 0 and 1 limit 1""", now=self.dueCutoff)
and queue between 0 and 1 limit 1""", now=self.dayCutoff)
def spacedCardCount(self):
"Number of spaced cards."
@ -1218,6 +425,9 @@ due > :now and due < :now""", now=time.time())
# Facts
##########################################################################
def factCount(self):
return self.db.scalar("select count() from facts")
def newFact(self, model=None):
"Return a new fact with the current model."
if model is None:
@ -1239,7 +449,6 @@ due > :now and due < :now""", now=time.time())
cards = []
self.db.save(fact)
# update field cache
self.factCount += 1
self.flushMod()
isRandom = self.newCardOrder == NEW_CARDS_RANDOM
if isRandom:
@ -1309,7 +518,6 @@ where factId = :fid and cardModelId = :cmid""",
fact, cardModel,
fact.created+0.0001*cardModel.ordinal)
self.updateCardTags([card.id])
self.cardCount += 1
raise Exception("incorrect; not checking selective study")
self.newAvail += 1
ids.append(card.id)
@ -1394,6 +602,9 @@ where facts.id not in (select distinct factId from cards)""")
# Cards
##########################################################################
def cardCount(self):
return self.db.scalar("select count() from cards")
def deleteCard(self, id):
"Delete a card given its id. Delete any unused facts. Don't flush."
self.deleteCards([id])
@ -1719,6 +930,32 @@ set modified = strftime("%s", "now")
where modelId = :id""", id=modelId)
self.flushMod()
def updateAllFieldChecksums(self):
# zero out
self.db.statement("update fields set chksum = ''")
# add back for unique fields
for m in self.models:
for fm in m.fieldModels:
self.updateFieldChecksums(fm.id)
def updateFieldChecksums(self, fmid):
self.db.flush()
self.setSchemaModified()
unique = self.db.scalar(
"select \"unique\" from fieldModels where id = :id", id=fmid)
if unique:
l = []
for (id, value) in self.db.all(
"select id, value from fields where fieldModelId = :id",
id=fmid):
l.append({'id':id, 'chk':fieldChecksum(value)})
self.db.statements(
"update fields set chksum = :chk where id = :id", l)
else:
self.db.statement(
"update fields set chksum = '' where fieldModelId=:id",
id=fmid)
# Card models
##########################################################################
@ -2504,7 +1741,7 @@ select cardId from cardTags where cardTags.tagId in %s""" % ids2str(ids)
qquery += " intersect "
elif isNeg:
qquery += "select id from cards except "
if token in ("rev", "new", "failed"):
if token in ("rev", "new", "lrn"):
if token == "rev":
n = 1
elif token == "new":
@ -2517,7 +1754,7 @@ select cardId from cardTags where cardTags.tagId in %s""" % ids2str(ids)
qquery += ("select id from cards where "
"due < %d and due > %d and "
"type in (0,1,2)") % (
self.dueCutoff, self.dueCutoff)
self.dayCutoff, self.dayCutoff)
elif token == "suspended":
qquery += ("select id from cards where "
"queue = -1")
@ -2527,7 +1764,7 @@ select cardId from cardTags where cardTags.tagId in %s""" % ids2str(ids)
"from deckvars where key = 'leechFails')")
else: # due
qquery += ("select id from cards where "
"queue between 0 and 1 and due < %d") % self.dueCutoff
"queue between 0 and 1 and due < %d") % self.dayCutoff
elif type == SEARCH_FID:
if fidquery:
if isNeg:

View file

@ -101,7 +101,8 @@ class AnkiExporter(Exporter):
copyLocalMedia(client.deck, server.deck)
# need to save manually
self.newDeck.rebuildCounts()
self.exportedCards = self.newDeck.cardCount
# FIXME
#self.exportedCards = self.newDeck.cardCount
self.newDeck.utcOffset = -1
self.newDeck.db.commit()
self.newDeck.close()

View file

@ -242,7 +242,6 @@ The current importer only supports a single active card template. Please disable
[fudgeCreated({'modelId': self.model.id,
'tags': canonifyTags(self.tagsToAdd + " " + cards[n].tags),
'id': factIds[n]}) for n in range(len(cards))])
self.deck.factCount += len(factIds)
self.deck.db.execute("""
delete from factsDeleted
where factId in (%s)""" % ",".join([str(s) for s in factIds]))
@ -285,7 +284,6 @@ where factId in (%s)""" % ",".join([str(s) for s in factIds]))
data)
self.deck.updateProgress()
self.deck.updateCardsFromFactIds(factIds)
self.deck.cardCount += len(cards) * active
self.total = len(factIds)
def addMeta(self, data, card):

685
anki/sched.py Normal file
View file

@ -0,0 +1,685 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import time, datetime
from heapq import *
from anki.db import *
from anki.cards import Card
from anki.utils import parseTags
# the standard Anki scheduler
class Scheduler(object):
def __init__(self, deck):
self.deck = deck
self.db = deck.db
self.name = "main"
self.queueLimit = 200
self.learnLimit = 1000
self.updateCutoff()
# restore any cards temporarily suspended by alternate schedulers
try:
self.resetSchedBuried()
except OperationalError, e:
# will fail if deck hasn't been upgraded yet
print "resetSched() failed"
def getCard(self, orm=True):
"Pop the next card from the queue. None if finished."
id = self._getCard()
if id:
card = Card()
assert card.fromDB(self.db, id)
return card
def reset(self):
self.resetLearn()
self.resetReview()
self.resetNew()
print "reset(); need to handle new cards"
# # day counts
# (self.repsToday, self.newSeenToday) = self.db.first("""
# select count(), sum(case when rep = 1 then 1 else 0 end) from revlog
# where time > :t""", t=self.dayCutoff-86400)
# self.newSeenToday = self.newSeenToday or 0
# print "newSeenToday in answer(), reset called twice"
# print "newSeenToday needs to account for drill mode too."
# FIXME: can we do this now with the learn queue?
def rebuildTypes(self):
"Rebuild the type cache. Only necessary on upgrade."
# set type first
self.db.statement("""
update cards set type = (case
when successive then 1 when reps then 0 else 2 end)
""")
# then queue
self.db.statement("""
update cards set queue = type
when queue != -1""")
# FIXME: merge these into the fetching code? rely on the type/queue
# properties? have to think about implications for cramming
def cardQueue(self, card):
return self.cardType(card)
def cardType(self, card):
"Return the type of the current card (what queue it's in)"
if card.successive:
return 1
elif card.reps:
return 0
else:
return 2
# Tools
##########################################################################
def resetSchedBuried(self):
"Put temporarily suspended cards back into play."
self.db.statement(
"update cards set queue = type where queue = -3")
def cardLimit(self, active, inactive, sql):
yes = parseTags(getattr(self.deck, active))
no = parseTags(getattr(self.deck, inactive))
if yes:
yids = tagIds(self.db, yes).values()
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and +c.id not in (select cardId from "
"cardTags where tagId in %s) and" % (
ids2str(yids),
ids2str(nids)))
elif no:
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id not in (select cardId from cardTags where "
"tagId in %s) and" % ids2str(nids))
else:
return sql
# Daily cutoff
##########################################################################
def updateCutoff(self):
d = datetime.datetime.utcfromtimestamp(
time.time() - self.deck.utcOffset) + datetime.timedelta(days=1)
d = datetime.datetime(d.year, d.month, d.day)
newday = self.deck.utcOffset - time.timezone
d += datetime.timedelta(seconds=newday)
cutoff = time.mktime(d.timetuple())
# cutoff must not be in the past
while cutoff < time.time():
cutoff += 86400
# cutoff must not be more than 24 hours in the future
cutoff = min(time.time() + 86400, cutoff)
self.dayCutoff = cutoff
self.dayCount = int(cutoff/86400 - self.deck.created/86400)
print "dayCount", self.dayCount
def checkDay(self):
# check if the day has rolled over
if time.time() > self.dayCutoff:
self.updateCutoff()
self.reset()
# Learning queue
##########################################################################
def resetLearn(self):
self.learnQueue = self.db.all("""
select due, id from cards where
queue = 0 and due < :lim order by due
limit %d""" % self.learnLimit, lim=self.dayCutoff)
self.learnQueue.reverse()
self.learnCount = len(self.learnQueue)
def getLearnCard(self):
if self.learnQueue and self.learnQueue[0] < time.time():
return heappop(self.learnQueue)
# Reviews
##########################################################################
def resetReview(self):
self.revCount = self.db.scalar(
self.cardLimit(
"revActive", "revInactive",
"select count(*) from cards c where queue = 1 "
"and due < :lim"), lim=self.dayCutoff)
self.revQueue = []
def getReviewCard(self):
if self.haveRevCards():
return self.revQueue.pop()
def haveRevCards(self):
if self.revCount:
if not self.revQueue:
self.fillRevQueue()
return self.revQueue
def fillRevQueue(self):
self.revQueue = self.db.all(
self.cardLimit(
"revActive", "revInactive", """
select c.id, factId from cards c where
queue = 1 and due < :lim order by %s
limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dayCutoff)
self.revQueue.reverse()
# FIXME: current random order won't work with new spacing
def revOrder(self):
return ("interval desc",
"interval",
"due",
"factId, ordinal")[self.revCardOrder]
# FIXME: rewrite
def showFailedLast(self):
return self.collapseTime or not self.delay0
# New cards
##########################################################################
# when do we do this?
#self.updateNewCountToday()
def resetNew(self):
# self.updateNewCardRatio()
pass
def rebuildNewCount(self):
self.newAvail = self.db.scalar(
self.cardLimit(
"newActive", "newInactive",
"select count(*) from cards c where queue = 2 "
"and due < :lim"), lim=self.dayCutoff)
self.updateNewCountToday()
self.spacedCards = []
def updateNewCountToday(self):
self.newCount = max(min(
self.newAvail, self.newCardsPerDay -
self.newSeenToday), 0)
def fillNewQueue(self):
if self.newCount and not self.newQueue and not self.spacedCards:
self.newQueue = self.db.all(
self.cardLimit(
"newActive", "newInactive", """
select c.id, factId from cards c where
queue = 2 and due < :lim order by %s
limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dayCutoff)
self.newQueue.reverse()
def updateNewCardRatio(self):
if self.newCardSpacing == 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)
else:
self.newCardModulus = 0
else:
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.newCardSpacing == NEW_CARDS_LAST:
return False
if self.newCardSpacing == NEW_CARDS_FIRST:
return True
if self.newCardModulus:
return self.repsToday % self.newCardModulus == 0
else:
return False
def getNewCard(self):
src = None
if (self.spacedCards and
self.spacedCards[0][0] < time.time()):
# spaced card has expired
src = 0
elif self.newQueue:
# card left in new queue
src = 1
elif self.spacedCards:
# card left in spaced queue
src = 0
else:
# only cards spaced to another day left
return
if src == 0:
cards = self.spacedCards[0][1]
self.newFromCache = True
return cards[0]
else:
self.newFromCache = False
return self.newQueue[-1][0]
def newOrder(self):
return ("due",
"due",
"due desc")[self.newCardOrder]
# Getting the next card
##########################################################################
def getCard(self):
"Return the next due card id, or None."
self.checkDay()
# learning card due?
id = self.getLearnCard()
if id:
return id
# distribute new cards?
if self.newNoSpaced() and self.timeForNewCard():
return self.getNewCard()
# card due for review?
if self.revNoSpaced():
return self.revQueue[-1][0]
# new cards left?
if self.newCount:
id = self.getNewCard()
if id:
return id
# display failed cards early/last
if not check and self.showFailedLast() and self.learnQueue:
return self.learnQueue[-1][0]
# if we're in a custom scheduler, we may need to switch back
if self.finishScheduler:
self.finishScheduler()
self.reset()
return self.getCardId()
# Answering a card
##########################################################################
def answerCard(self, card, ease):
undoName = _("Answer Card")
self.setUndoStart(undoName)
now = time.time()
# old state
oldState = self.cardState(card)
oldQueue = self.cardQueue(card)
lastDelaySecs = time.time() - card.due
lastDelay = lastDelaySecs / 86400.0
oldSuc = card.successive
# update card details
last = card.interval
card.interval = self.nextInterval(card, ease)
card.lastInterval = last
if card.reps:
# only update if card was not new
card.lastDue = card.due
card.due = self.nextDue(card, ease, oldState)
if not self.finishScheduler:
# don't update factor in custom schedulers
self.updateFactor(card, ease)
# spacing
self.spaceCards(card)
# adjust counts for current card
if ease == 1:
if card.due < self.dayCutoff:
self.learnCount += 1
if oldQueue == 0:
self.learnCount -= 1
elif oldQueue == 1:
self.revCount -= 1
else:
self.newAvail -= 1
# card stats
self.updateCardStats(card, ease, oldState)
# update type & ensure past cutoff
card.type = self.cardType(card)
card.queue = card.type
if ease != 1:
card.due = max(card.due, self.dayCutoff+1)
# allow custom schedulers to munge the card
if self.answerPreSave:
self.answerPreSave(card, ease)
# save
card.due = card.due
card.toDB(self.db)
# review history
print "make sure flags is set correctly when reviewing early"
logReview(self.db, card, ease, 0)
self.modified = now
# leech handling - we need to do this after the queue, as it may cause
# a reset()
isLeech = self.isLeech(card)
if isLeech:
self.handleLeech(card)
runHook("cardAnswered", card.id, isLeech)
self.setUndoEnd(undoName)
def updateCardStats(self, card, ease, state):
card.reps += 1
if ease == 1:
card.successive = 0
card.lapses += 1
else:
card.successive += 1
# if not card.firstAnswered:
# card.firstAnswered = time.time()
card.setModified()
def spaceCards(self, card):
new = time.time() + self.newSpacing
self.db.statement("""
update cards set
due = (case
when queue = 1 then due + 86400 * (case
when interval*:rev < 1 then 0
else interval*:rev
end)
when queue = 2 then :new
end),
modified = :now
where id != :id and factId = :factId
and due < :cut
and queue between 1 and 2""",
id=card.id, now=time.time(), factId=card.factId,
cut=self.dayCutoff, new=new, rev=self.revSpacing)
# update local cache of seen facts
self.spacedFacts[card.factId] = new
# Interval management
##########################################################################
def nextInterval(self, card, ease):
"Return the next interval for CARD given EASE."
delay = self.adjustedDelay(card, ease)
return self._nextInterval(card, delay, ease)
def _nextInterval(self, card, delay, ease):
interval = card.interval
factor = card.factor
# if cramming / reviewing early
if delay < 0:
interval = max(card.lastInterval, card.interval + delay)
if interval < self.midIntervalMin:
interval = 0
delay = 0
# if interval is less than mid interval, use presets
if ease == 1:
interval *= self.delay2
if interval < self.hardIntervalMin:
interval = 0
elif interval == 0:
if ease == 2:
interval = random.uniform(self.hardIntervalMin,
self.hardIntervalMax)
elif ease == 3:
interval = random.uniform(self.midIntervalMin,
self.midIntervalMax)
elif ease == 4:
interval = random.uniform(self.easyIntervalMin,
self.easyIntervalMax)
else:
# if not cramming, boost initial 2
if (interval < self.hardIntervalMax and
interval > 0.166):
mid = (self.midIntervalMin + self.midIntervalMax) / 2.0
interval = mid / factor
# multiply last interval by factor
if ease == 2:
interval = (interval + delay/4) * 1.2
elif ease == 3:
interval = (interval + delay/2) * factor
elif ease == 4:
interval = (interval + delay) * factor * self.factorFour
fuzz = random.uniform(0.95, 1.05)
interval *= fuzz
return interval
def nextIntervalStr(self, card, ease, short=False):
"Return the next interval for CARD given EASE as a string."
int = self.nextInterval(card, ease)
return anki.utils.fmtTimeSpan(int*86400, short=short)
def nextDue(self, card, ease, oldState):
"Return time when CARD will expire given EASE."
if ease == 1:
# 600 is a magic value which means no bonus, and is used to ease
# upgrades
cram = self.scheduler == "cram"
if (not cram and oldState == "mature"
and self.delay1 and self.delay1 != 600):
# user wants a bonus of 1+ days. put the failed cards at the
# start of the future day, so that failures that day will come
# after the waiting cards
return self.dayCutoff + (self.delay1 - 1)*86400
else:
due = 0
else:
due = card.interval * 86400.0
return due + time.time()
def updateFactor(self, card, ease):
"Update CARD's factor based on EASE."
print "update cardIsBeingLearnt()"
if not card.reps:
# card is new, inherit beginning factor
card.factor = self.averageFactor
if card.successive and not self.cardIsBeingLearnt(card):
if ease == 1:
card.factor -= 0.20
elif ease == 2:
card.factor -= 0.15
if ease == 4:
card.factor += 0.10
card.factor = max(1.3, card.factor)
def adjustedDelay(self, card, ease):
"Return an adjusted delay value for CARD based on EASE."
if self.cardIsNew(card):
return 0
if card.due <= self.dayCutoff:
return (self.dayCutoff - card.due) / 86400.0
else:
return (self.dayCutoff - card.due) / 86400.0
# Leeches
##########################################################################
def isLeech(self, card):
no = card.lapses
fmax = self.getInt('leechFails')
if not fmax:
return
return (
# failed
not card.successive and
# greater than fail threshold
no >= fmax and
# at least threshold/2 reps since last time
(fmax - no) % (max(fmax/2, 1)) == 0)
def handleLeech(self, card):
self.refreshSession()
scard = self.cardFromId(card.id, True)
tags = scard.fact.tags
tags = addTags("Leech", tags)
scard.fact.tags = canonifyTags(tags)
scard.fact.setModified(textChanged=True, deck=self)
self.updateFactTags([scard.fact.id])
self.db.flush()
self.db.expunge(scard)
if self.getBool('suspendLeeches'):
self.suspendCards([card.id])
self.reset()
self.refreshSession()
# Review early
##########################################################################
def setupReviewEarlyScheduler(self):
self.fillRevQueue = self._fillRevEarlyQueue
self.rebuildRevCount = self._rebuildRevEarlyCount
self.finishScheduler = self.setupStandardScheduler
self.answerPreSave = self._reviewEarlyPreSave
self.scheduler = "reviewEarly"
def _reviewEarlyPreSave(self, card, ease):
if ease > 1:
# prevent it from appearing in next queue fill
card.queue = -3
def _rebuildRevEarlyCount(self):
# in the future it would be nice to skip the first x days of due cards
self.revCount = self.db.scalar(
self.cardLimit(
"revActive", "revInactive", """
select count() from cards c where queue = 1 and due > :now
"""), now=self.dayCutoff)
def _fillRevEarlyQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.db.all(
self.cardLimit(
"revActive", "revInactive", """
select id, factId from cards c where queue = 1 and due > :lim
order by due limit %d""" % self.queueLimit), lim=self.dayCutoff)
self.revQueue.reverse()
# Learn more
##########################################################################
def setupLearnMoreScheduler(self):
self.rebuildNewCount = self._rebuildLearnMoreCount
self.updateNewCountToday = self._updateLearnMoreCountToday
self.finishScheduler = self.setupStandardScheduler
self.scheduler = "learnMore"
def _rebuildLearnMoreCount(self):
self.newAvail = self.db.scalar(
self.cardLimit(
"newActive", "newInactive",
"select count(*) from cards c where queue = 2 "
"and due < :lim"), lim=self.dayCutoff)
self.spacedCards = []
def _updateLearnMoreCountToday(self):
self.newCount = self.newAvail
# Cramming
##########################################################################
def setupCramScheduler(self, active, order):
self.getCardId = self._getCramCardId
self.activeCramTags = active
self.cramOrder = order
self.rebuildNewCount = self._rebuildCramNewCount
self.rebuildRevCount = self._rebuildCramCount
self.rebuildLrnCount = self._rebuildLrnCramCount
self.fillRevQueue = self._fillCramQueue
self.fillLrnQueue = self._fillLrnCramQueue
self.finishScheduler = self.setupStandardScheduler
self.lrnCramQueue = []
print "requeue cram"
self.requeueCard = self._requeueCramCard
self.cardQueue = self._cramCardQueue
self.answerCard = self._answerCramCard
self.spaceCards = self._spaceCramCards
# reuse review early's code
self.answerPreSave = self._cramPreSave
self.cardLimit = self._cramCardLimit
self.scheduler = "cram"
def _cramPreSave(self, card, ease):
# prevent it from appearing in next queue fill
card.lastInterval = self.cramLastInterval
card.type = -3
def _spaceCramCards(self, card):
self.spacedFacts[card.factId] = time.time() + self.newSpacing
def _answerCramCard(self, card, ease):
self.cramLastInterval = card.lastInterval
self._answerCard(card, ease)
if ease == 1:
self.lrnCramQueue.insert(0, [card.id, card.factId])
def _getCramCardId(self, check=True):
self.checkDay()
self.fillQueues()
if self.lrnCardMax and self.lrnCount >= self.lrnCardMax:
return self.lrnQueue[-1][0]
# card due for review?
if self.revNoSpaced():
return self.revQueue[-1][0]
if self.lrnQueue:
return self.lrnQueue[-1][0]
if check:
# collapse spaced cards before reverting back to old scheduler
self.reset()
return self.getCardId(False)
# if we're in a custom scheduler, we may need to switch back
if self.finishScheduler:
self.finishScheduler()
self.reset()
return self.getCardId()
def _cramCardQueue(self, card):
if self.revQueue and self.revQueue[-1][0] == card.id:
return 1
else:
return 0
def _requeueCramCard(self, card, oldSuc):
if self.cardQueue(card) == 1:
self.revQueue.pop()
else:
self.lrnCramQueue.pop()
def _rebuildCramNewCount(self):
self.newAvail = 0
self.newCount = 0
def _cramCardLimit(self, active, inactive, sql):
# inactive is (currently) ignored
if isinstance(active, list):
return sql.replace(
"where", "where +c.id in " + ids2str(active) + " and")
else:
yes = parseTags(active)
if yes:
yids = tagIds(self.db, yes).values()
return sql.replace(
"where ",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and " % ids2str(yids))
else:
return sql
def _fillCramQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.db.all(self.cardLimit(
self.activeCramTags, "", """
select id, factId from cards c
where queue between 0 and 2
order by %s
limit %s""" % (self.cramOrder, self.queueLimit)))
self.revQueue.reverse()
def _rebuildCramCount(self):
self.revCount = self.db.scalar(self.cardLimit(
self.activeCramTags, "",
"select count(*) from cards c where queue between 0 and 2"))
def _rebuildLrnCramCount(self):
self.lrnCount = len(self.lrnCramQueue)
def _fillLrnCramQueue(self):
self.lrnQueue = self.lrnCramQueue

View file

@ -75,7 +75,7 @@ class DeckStats(object):
d = self.deck
html="<h1>" + _("Deck Statistics") + "</h1>"
html += _("Deck created: <b>%s</b> ago<br>") % self.createdTimeStr()
total = d.cardCount
total = d.cardCount()
new = d.newCountAll()
young = d.youngCardCount()
old = d.matureCardCount()
@ -87,7 +87,7 @@ class DeckStats(object):
(stats["old"], stats["oldP"]) = (old, oldP)
(stats["young"], stats["youngP"]) = (young, youngP)
html += _("Total number of cards:") + " <b>%d</b><br>" % total
html += _("Total number of facts:") + " <b>%d</b><br><br>" % d.factCount
html += _("Total number of facts:") + " <b>%d</b><br><br>" % d.factCount()
html += "<b>" + _("Card Maturity") + "</b><br>"
html += _("Mature cards: <!--card count-->") + " <b>%(old)d</b> (%(oldP)s)<br>" % {
@ -118,7 +118,7 @@ class DeckStats(object):
'partOf' : nYes,
'totalSum' : nAll } + "<br><br>")
# average pending time
existing = d.cardCount - d.newCount
existing = d.cardCount() - d.newCount
def tr(a, b):
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
def repsPerDay(reps,days):
@ -292,7 +292,7 @@ where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset)
def newAverage(self):
"Average number of new cards added each day."
return self.deck.cardCount / max(1, self.ageInDays())
return self.deck.cardCount() / max(1, self.ageInDays())
def createdTimeStr(self):
return anki.utils.fmtTimeSpan(time.time() - self.deck.created)

View file

@ -34,14 +34,14 @@ def test_export_anki():
assert deck.modified == oldTime
# connect to new deck
d2 = Deck(newname, backup=False)
assert d2.cardCount == 4
assert d2.cardCount() == 4
# try again, limited to a tag
newname = unicode(tempfile.mkstemp(prefix="ankitest")[1])
os.unlink(newname)
e.limitTags = ['tag']
e.exportInto(newname)
d2 = Deck(newname, backup=False)
assert d2.cardCount == 2
assert d2.cardCount() == 2
@nose.with_setup(setup1)
def test_export_textcard():

View file

@ -89,11 +89,11 @@ def test_anki10_modtime():
f = deck1.newFact()
f['Front'] = u"foo"; f['Back'] = u"bar"
deck1.addFact(f)
assert deck1.cardCount == 1
assert deck2.cardCount == 0
assert deck1.cardCount() == 1
assert deck2.cardCount() == 0
client.sync()
assert deck1.cardCount == 1
assert deck2.cardCount == 1
assert deck1.cardCount() == 1
assert deck2.cardCount() == 1
file_ = unicode(os.path.join(testDir, "importing/test10-3.anki"))
file = "/tmp/test10-3.anki"
shutil.copy(file_, file)
@ -108,7 +108,7 @@ def test_anki10_modtime():
def test_dingsbums():
deck = Deck()
deck.addModel(BasicModel())
startNumberOfFacts = deck.factCount
startNumberOfFacts = deck.factCount()
file = unicode(os.path.join(testDir, "importing/dingsbums.xml"))
i = dingsbums.DingsBumsImporter(deck, file)
i.doImport()

View file

@ -56,8 +56,8 @@ def teardown():
@nose.with_setup(setup_local, teardown)
def test_localsync_diffing():
assert deck1.cardCount == 2
assert deck2.cardCount == 2
assert deck1.cardCount() == 2
assert deck2.cardCount() == 2
lsum = client.summary(deck1.lastSync)
rsum = server.summary(deck1.lastSync)
result = client.diffSummary(lsum, rsum, 'cards')
@ -157,12 +157,12 @@ def test_localsync_models():
@nose.with_setup(setup_local, teardown)
def test_localsync_factsandcards():
assert deck1.factCount == 1 and deck1.cardCount == 2
assert deck2.factCount == 1 and deck2.cardCount == 2
assert deck1.factCount() == 1 and deck1.cardCount() == 2
assert deck2.factCount() == 1 and deck2.cardCount() == 2
client.sync()
deck1.reset(); deck2.reset()
assert deck1.factCount == 2 and deck1.cardCount == 4
assert deck2.factCount == 2 and deck2.cardCount == 4
assert deck1.factCount() == 2 and deck1.cardCount() == 4
assert deck2.factCount() == 2 and deck2.cardCount() == 4
# ensure the fact was copied across
f1 = deck1.db.query(Fact).first()
f2 = deck1.db.query(Fact).get(f1.id)
@ -193,20 +193,20 @@ def test_localsync_threeway():
f = deck1.addFact(f)
card = f.cards[0]
client.sync()
assert deck1.cardCount == 6
assert deck2.cardCount == 6
assert deck1.cardCount() == 6
assert deck2.cardCount() == 6
# check it propagates from server to deck3
client2.sync()
assert deck3.cardCount == 6
assert deck3.cardCount() == 6
# delete a card on deck1
deck1.deleteCard(card.id)
client.sync()
deck1.reset(); deck2.reset()
assert deck1.cardCount == 5
assert deck2.cardCount == 5
assert deck1.cardCount() == 5
assert deck2.cardCount() == 5
# make sure the delete is now propagated from the server to deck3
client2.sync()
assert deck3.cardCount == 5
assert deck3.cardCount() == 5
def test_localsync_media():
tmpdir = "/tmp/media-tests"

View file

@ -72,20 +72,20 @@ def test_saveAs():
f = deck.newFact()
f['Front'] = u"foo"; f['Back'] = u"bar"
deck.addFact(f)
assert deck.cardCount == 1
assert deck.cardCount() == 1
# save in new deck
newDeck = deck.saveAs(path)
assert newDeck.cardCount == 1
assert newDeck.cardCount() == 1
# delete card
id = newDeck.db.scalar("select id from cards")
newDeck.deleteCard(id)
# save into new deck
newDeck2 = newDeck.saveAs(path2)
# new deck should have zero cards
assert newDeck2.cardCount == 0
assert newDeck2.cardCount() == 0
# but old deck should have reverted the unsaved changes
newDeck = Deck(path)
assert newDeck.cardCount == 1
assert newDeck.cardCount() == 1
newDeck.close()
def test_factAddDelete():
@ -171,10 +171,10 @@ def test_modelAddDelete():
f['Front'] = u'1'
f['Back'] = u'2'
deck.addFact(f)
assert deck.cardCount == 1
assert deck.cardCount() == 1
deck.deleteModel(deck.currentModel)
deck.reset()
assert deck.cardCount == 0
assert deck.cardCount() == 0
deck.db.refresh(deck)
def test_modelCopy():
@ -249,8 +249,8 @@ def test_modelChange():
# convert to basic
assert deck.modelUseCount(m1) == 2
assert deck.modelUseCount(m2) == 0
assert deck.cardCount == 4
assert deck.factCount == 2
assert deck.cardCount() == 4
assert deck.factCount() == 2
fmap = {m1.fieldModels[0]: m2.fieldModels[0],
m1.fieldModels[1]: None,
m1.fieldModels[2]: m2.fieldModels[1]}
@ -260,8 +260,8 @@ def test_modelChange():
deck.reset()
assert deck.modelUseCount(m1) == 1
assert deck.modelUseCount(m2) == 1
assert deck.cardCount == 3
assert deck.factCount == 2
assert deck.cardCount() == 3
assert deck.factCount() == 2
(q, a) = deck.db.first("""
select question, answer from cards where factId = :id""",
id=f.id)