mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
moving scheduling code into separate file, some preliminary refactoring
This commit is contained in:
parent
8cbd3c9f3b
commit
4e7e8b03bc
10 changed files with 784 additions and 855 deletions
|
@ -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
|
||||
|
|
871
anki/deck.py
871
anki/deck.py
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
685
anki/sched.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue