refactor cards

Cards had developed quite a lot of cruft from incremental changes, and a
number of important attributes were stored in names that had no bearing to
their actual use.

Added:

- position, which new cards will be sorted on in the future
- flags, which is reserved for future use

Renamed:

- type to queue
- relativeDelay to type
- noCount to lapses

Removed:

- all new/young/matureEase counts; the information is in the revlog
- firstAnswered, lastDue, lastFactor, averageTime and totalTime for the same
  reason
- isDue, spaceUntil and combinedDue, because they are no longer used. Spaced
  cards will be implemented differently in a coming commit.
- priority
- yesCount, because it can be inferred from reps & lapses
- tags; they've been stored in facts for a long time now

Also compatibility with deck versions less than 65 has been dropped, so decks
will need to be upgraded to 1.2 before they can be upgraded by the dev code.
All shared decks are on 1.2, so this should hopefully not be a problem.
This commit is contained in:
Damien Elmes 2011-02-21 10:21:28 +09:00
parent f828393de3
commit 9aa2f8dc40
8 changed files with 235 additions and 440 deletions

View file

@ -14,72 +14,50 @@ MAX_TIMER = 60
# Cards # Cards
########################################################################## ##########################################################################
# Type: 0=lapsed, 1=due, 2=new, 3=drilled
# Queue: under normal circumstances, same as type.
# -1=suspended, -2=user buried, -3=sched buried (rev early, etc)
# Ordinal: card template # for fact
# Position: sorting position, only for new cards
# Flags: unused; reserved for future use
cardsTable = Table( cardsTable = Table(
'cards', metadata, 'cards', metadata,
Column('id', Integer, primary_key=True), Column('id', Integer, primary_key=True),
Column('factId', Integer, ForeignKey("facts.id"), nullable=False), Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False), Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
# general
Column('created', Float, nullable=False, default=time.time), Column('created', Float, nullable=False, default=time.time),
Column('modified', Float, nullable=False, default=time.time), Column('modified', Float, nullable=False, default=time.time),
Column('tags', UnicodeText, nullable=False, default=u""),
Column('ordinal', Integer, nullable=False),
# q/a cached - changed on fact update
Column('question', UnicodeText, nullable=False, default=u""), Column('question', UnicodeText, nullable=False, default=u""),
Column('answer', UnicodeText, nullable=False, default=u""), Column('answer', UnicodeText, nullable=False, default=u""),
Column('priority', Integer, nullable=False, default=2), # obsolete Column('flags', Integer, nullable=False, default=0),
Column('interval', Float, nullable=False, default=0), # ordering
Column('ordinal', Integer, nullable=False),
Column('position', Integer, nullable=False),
# scheduling data
Column('type', Integer, nullable=False, default=2),
Column('queue', Integer, nullable=False, default=2),
Column('lastInterval', Float, nullable=False, default=0), Column('lastInterval', Float, nullable=False, default=0),
Column('due', Float, nullable=False, default=time.time), Column('interval', Float, nullable=False, default=0),
Column('lastDue', Float, nullable=False, default=0), Column('due', Float, nullable=False),
Column('factor', Float, nullable=False, default=2.5), Column('factor', Float, nullable=False, default=2.5),
Column('lastFactor', Float, nullable=False, default=2.5), # counters
Column('firstAnswered', Float, nullable=False, default=0),
# stats
Column('reps', Integer, nullable=False, default=0), Column('reps', Integer, nullable=False, default=0),
Column('successive', Integer, nullable=False, default=0), Column('successive', Integer, nullable=False, default=0),
Column('averageTime', Float, nullable=False, default=0), Column('lapses', Integer, nullable=False, default=0))
Column('reviewTime', Float, nullable=False, default=0),
Column('youngEase0', Integer, nullable=False, default=0),
Column('youngEase1', Integer, nullable=False, default=0),
Column('youngEase2', Integer, nullable=False, default=0),
Column('youngEase3', Integer, nullable=False, default=0),
Column('youngEase4', Integer, nullable=False, default=0),
Column('matureEase0', Integer, nullable=False, default=0),
Column('matureEase1', Integer, nullable=False, default=0),
Column('matureEase2', Integer, nullable=False, default=0),
Column('matureEase3', Integer, nullable=False, default=0),
Column('matureEase4', Integer, nullable=False, default=0),
# this duplicates the above data, because there's no way to map imported
# data to the above
Column('yesCount', Integer, nullable=False, default=0),
Column('noCount', Integer, nullable=False, default=0),
# obsolete
Column('spaceUntil', Float, nullable=False, default=0),
# relativeDelay is reused as type without scheduling (ie, it remains 0-2
# even if card is suspended, etc)
Column('relativeDelay', Float, nullable=False, default=0),
Column('isDue', Boolean, nullable=False, default=0), # obsolete
Column('type', Integer, nullable=False, default=2),
Column('combinedDue', Integer, nullable=False, default=0))
class Card(object): class Card(object):
"A card."
def __init__(self, fact=None, cardModel=None, created=None): def __init__(self, fact=None, cardModel=None, created=None):
self.tags = u""
self.id = genID() self.id = genID()
# new cards start as new & due
self.type = 2
self.relativeDelay = self.type
self.timerStarted = False
self.timerStopped = False
self.modified = time.time() self.modified = time.time()
if created: if created:
self.created = created self.created = created
self.due = created self.due = created
else: else:
self.due = self.modified self.due = self.modified
self.combinedDue = self.due self.position = self.due
if fact: if fact:
self.fact = fact self.fact = fact
if cardModel: if cardModel:
@ -87,13 +65,27 @@ class Card(object):
# for non-orm use # for non-orm use
self.cardModelId = cardModel.id self.cardModelId = cardModel.id
self.ordinal = cardModel.ordinal self.ordinal = cardModel.ordinal
# timer
self.timerStarted = None
def setModified(self):
self.modified = time.time()
def startTimer(self):
self.timerStarted = time.time()
def userTime(self):
return min(time.time() - self.timerStarted, MAX_TIMER)
# Questions and answers
##########################################################################
def rebuildQA(self, deck, media=True): def rebuildQA(self, deck, media=True):
# format qa # format qa
d = {} d = {}
for f in self.fact.model.fieldModels: for f in self.fact.model.fieldModels:
d[f.name] = (f.id, self.fact[f.name]) d[f.name] = (f.id, self.fact[f.name])
qa = formatQA(None, self.fact.modelId, d, self.splitTags(), qa = formatQA(None, self.fact.modelId, d, self._splitTags(),
self.cardModel, deck) self.cardModel, deck)
# find old media references # find old media references
files = {} files = {}
@ -119,25 +111,6 @@ class Card(object):
updateMediaCount(deck, f, cnt) updateMediaCount(deck, f, cnt)
self.setModified() self.setModified()
def setModified(self):
self.modified = time.time()
def startTimer(self):
self.timerStarted = time.time()
def stopTimer(self):
self.timerStopped = time.time()
def thinkingTime(self):
return (self.timerStopped or time.time()) - self.timerStarted
def totalTime(self):
return time.time() - self.timerStarted
def genFuzz(self):
"Generate a random offset to spread intervals."
self.fuzz = random.uniform(0.95, 1.05)
def htmlQuestion(self, type="question", align=True): def htmlQuestion(self, type="question", align=True):
div = '''<div class="card%s" id="cm%s%s">%s</div>''' % ( div = '''<div class="card%s" id="cm%s%s">%s</div>''' % (
type[0], type[0], hexifyID(self.cardModelId), type[0], type[0], hexifyID(self.cardModelId),
@ -158,52 +131,14 @@ class Card(object):
def htmlAnswer(self, align=True): def htmlAnswer(self, align=True):
return self.htmlQuestion(type="answer", align=align) return self.htmlQuestion(type="answer", align=align)
def updateStats(self, ease, state): def _splitTags(self):
self.reps += 1
if ease > 1:
self.successive += 1
else:
self.successive = 0
delay = min(self.totalTime(), MAX_TIMER)
self.reviewTime += delay
if self.averageTime:
self.averageTime = (self.averageTime + delay) / 2.0
else:
self.averageTime = delay
# we don't track first answer for cards
if state == "new":
state = "young"
# update ease and yes/no count
attr = state + "Ease%d" % ease
setattr(self, attr, getattr(self, attr) + 1)
if ease < 2:
self.noCount += 1
else:
self.yesCount += 1
if not self.firstAnswered:
self.firstAnswered = time.time()
self.setModified()
def splitTags(self):
return (self.fact.tags, self.fact.model.tags, self.cardModel.name) return (self.fact.tags, self.fact.model.tags, self.cardModel.name)
def allTags(self): # Non-ORM
"Non-canonified string of all tags." ##########################################################################
return (self.fact.tags + "," +
self.fact.model.tags)
def hasTag(self, tag):
return findTag(tag, parseTags(self.allTags()))
def fromDB(self, s, id): def fromDB(self, s, id):
r = s.first("""select r = s.first("""select * from cards where id = :id""", id=id)
id, factId, cardModelId, created, modified, tags, ordinal, question, answer,
priority, interval, lastInterval, due, lastDue, factor,
lastFactor, firstAnswered, reps, successive, averageTime, reviewTime,
youngEase0, youngEase1, youngEase2, youngEase3, youngEase4,
matureEase0, matureEase1, matureEase2, matureEase3, matureEase4,
yesCount, noCount, spaceUntil, isDue, type, combinedDue
from cards where id = :id""", id=id)
if not r: if not r:
return return
(self.id, (self.id,
@ -211,74 +146,42 @@ from cards where id = :id""", id=id)
self.cardModelId, self.cardModelId,
self.created, self.created,
self.modified, self.modified,
self.tags,
self.ordinal,
self.question, self.question,
self.answer, self.answer,
self.priority, self.flags,
self.interval, self.ordinal,
self.position,
self.type,
self.queue,
self.lastInterval, self.lastInterval,
self.interval,
self.due, self.due,
self.lastDue,
self.factor, self.factor,
self.lastFactor,
self.firstAnswered,
self.reps, self.reps,
self.successive, self.successive,
self.averageTime, self.lapses) = r
self.reviewTime,
self.youngEase0,
self.youngEase1,
self.youngEase2,
self.youngEase3,
self.youngEase4,
self.matureEase0,
self.matureEase1,
self.matureEase2,
self.matureEase3,
self.matureEase4,
self.yesCount,
self.noCount,
self.spaceUntil,
self.isDue,
self.type,
self.combinedDue) = r
return True return True
def toDB(self, s): def toDB(self, s):
"Write card to DB."
s.execute("""update cards set s.execute("""update cards set
factId=:factId,
cardModelId=:cardModelId,
created=:created,
modified=:modified, modified=:modified,
tags=:tags, question=:question,
interval=:interval, answer=:answer,
flags=:flags,
ordinal=:ordinal,
position=:position,
type=:type,
queue=:queue,
lastInterval=:lastInterval, lastInterval=:lastInterval,
interval=:interval,
due=:due, due=:due,
lastDue=:lastDue,
factor=:factor, factor=:factor,
lastFactor=:lastFactor,
firstAnswered=:firstAnswered,
reps=:reps, reps=:reps,
successive=:successive, successive=:successive,
averageTime=:averageTime, lapses=:lapses
reviewTime=:reviewTime,
youngEase0=:youngEase0,
youngEase1=:youngEase1,
youngEase2=:youngEase2,
youngEase3=:youngEase3,
youngEase4=:youngEase4,
matureEase0=:matureEase0,
matureEase1=:matureEase1,
matureEase2=:matureEase2,
matureEase3=:matureEase3,
matureEase4=:matureEase4,
yesCount=:yesCount,
noCount=:noCount,
spaceUntil = :spaceUntil,
isDue = 0,
type = :type,
combinedDue = :combinedDue,
relativeDelay = :relativeDelay,
priority = :priority
where id=:id""", self.__dict__) where id=:id""", self.__dict__)
mapper(Card, cardsTable, properties={ mapper(Card, cardsTable, properties={
@ -292,7 +195,6 @@ mapper(Fact, factsTable, properties={
'fields': relation(Field, backref="fact", order_by=Field.ordinal), 'fields': relation(Field, backref="fact", order_by=Field.ordinal),
}) })
# Card deletions # Card deletions
########################################################################## ##########################################################################

View file

@ -14,7 +14,7 @@ from anki.utils import parseTags, tidyHTML, genID, ids2str, hexifyID, \
from anki.revlog import logReview from anki.revlog import logReview
from anki.models import Model, CardModel, formatQA from anki.models import Model, CardModel, formatQA
from anki.fonts import toPlatformFont from anki.fonts import toPlatformFont
from anki.tags import initTagTables, tagIds from anki.tags import initTagTables, tagIds, tagId
from operator import itemgetter from operator import itemgetter
from itertools import groupby from itertools import groupby
from anki.hooks import runHook, hookEmpty from anki.hooks import runHook, hookEmpty
@ -242,22 +242,22 @@ where time > :t""", t=self.failedCutoff-86400)
self.failedSoonCount = self.db.scalar( self.failedSoonCount = self.db.scalar(
self.cardLimit( self.cardLimit(
"revActive", "revInactive", "revActive", "revInactive",
"select count(*) from cards c where type = 0 " "select count(*) from cards c where queue = 0 "
"and combinedDue < :lim"), lim=self.failedCutoff) "and due < :lim"), lim=self.failedCutoff)
def _rebuildRevCount(self): def _rebuildRevCount(self):
self.revCount = self.db.scalar( self.revCount = self.db.scalar(
self.cardLimit( self.cardLimit(
"revActive", "revInactive", "revActive", "revInactive",
"select count(*) from cards c where type = 1 " "select count(*) from cards c where queue = 1 "
"and combinedDue < :lim"), lim=self.dueCutoff) "and due < :lim"), lim=self.dueCutoff)
def _rebuildNewCount(self): def _rebuildNewCount(self):
self.newAvail = self.db.scalar( self.newAvail = self.db.scalar(
self.cardLimit( self.cardLimit(
"newActive", "newInactive", "newActive", "newInactive",
"select count(*) from cards c where type = 2 " "select count(*) from cards c where queue = 2 "
"and combinedDue < :lim"), lim=self.dueCutoff) "and due < :lim"), lim=self.dueCutoff)
self.updateNewCountToday() self.updateNewCountToday()
self.spacedCards = [] self.spacedCards = []
@ -271,8 +271,8 @@ where time > :t""", t=self.failedCutoff-86400)
self.failedQueue = self.db.all( self.failedQueue = self.db.all(
self.cardLimit( self.cardLimit(
"revActive", "revInactive", """ "revActive", "revInactive", """
select c.id, factId, combinedDue from cards c where select c.id, factId, due from cards c where
type = 0 and combinedDue < :lim order by combinedDue queue = 0 and due < :lim order by due
limit %d""" % self.queueLimit), lim=self.failedCutoff) limit %d""" % self.queueLimit), lim=self.failedCutoff)
self.failedQueue.reverse() self.failedQueue.reverse()
@ -282,7 +282,7 @@ limit %d""" % self.queueLimit), lim=self.failedCutoff)
self.cardLimit( self.cardLimit(
"revActive", "revInactive", """ "revActive", "revInactive", """
select c.id, factId from cards c where select c.id, factId from cards c where
type = 1 and combinedDue < :lim order by %s queue = 1 and due < :lim order by %s
limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dueCutoff) limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dueCutoff)
self.revQueue.reverse() self.revQueue.reverse()
@ -292,7 +292,7 @@ limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dueCutoff)
self.cardLimit( self.cardLimit(
"newActive", "newInactive", """ "newActive", "newInactive", """
select c.id, factId from cards c where select c.id, factId from cards c where
type = 2 and combinedDue < :lim order by %s queue = 2 and due < :lim order by %s
limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dueCutoff) limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dueCutoff)
self.newQueue.reverse() self.newQueue.reverse()
@ -365,7 +365,7 @@ New type: %s""" % (self.failedSoonCount, self.revCount, self.newCount,
def revOrder(self): def revOrder(self):
return ("interval desc", return ("interval desc",
"interval", "interval",
"combinedDue", "due",
"factId, ordinal")[self.revCardOrder] "factId, ordinal")[self.revCardOrder]
def newOrder(self): def newOrder(self):
@ -375,18 +375,15 @@ New type: %s""" % (self.failedSoonCount, self.revCount, self.newCount,
def rebuildTypes(self): def rebuildTypes(self):
"Rebuild the type cache. Only necessary on upgrade." "Rebuild the type cache. Only necessary on upgrade."
# set canonical type first # set type first
self.db.statement(""" self.db.statement("""
update cards set update cards set type = (case
relativeDelay = (case
when successive then 1 when reps then 0 else 2 end) when successive then 1 when reps then 0 else 2 end)
""") """)
# then current type based on that # then queue
self.db.statement(""" self.db.statement("""
update cards set update cards set queue = type
type = (case when queue != -1""")
when type >= 0 then relativeDelay else relativeDelay - 3 end)
""")
def updateAllFieldChecksums(self): def updateAllFieldChecksums(self):
# zero out # zero out
@ -490,12 +487,12 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
def _reviewEarlyPreSave(self, card, ease): def _reviewEarlyPreSave(self, card, ease):
if ease > 1: if ease > 1:
# prevent it from appearing in next queue fill # prevent it from appearing in next queue fill
card.type += 6 card.queue = -3
def resetAfterReviewEarly(self): def resetAfterReviewEarly(self):
"Put temporarily suspended cards back into play. Caller must .reset()" "Put temporarily suspended cards back into play. Caller must .reset()"
self.db.statement( self.db.statement(
"update cards set type = type - 6 where type between 6 and 8") "update cards set queue = type where queue = -3")
def _onReviewEarlyFinished(self): def _onReviewEarlyFinished(self):
# clean up buried cards # clean up buried cards
@ -508,7 +505,7 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
self.revCount = self.db.scalar( self.revCount = self.db.scalar(
self.cardLimit( self.cardLimit(
"revActive", "revInactive", """ "revActive", "revInactive", """
select count() from cards c where type = 1 and combinedDue > :now select count() from cards c where queue = 1 and due > :now
"""), now=self.dueCutoff) """), now=self.dueCutoff)
def _fillRevEarlyQueue(self): def _fillRevEarlyQueue(self):
@ -516,8 +513,8 @@ select count() from cards c where type = 1 and combinedDue > :now
self.revQueue = self.db.all( self.revQueue = self.db.all(
self.cardLimit( self.cardLimit(
"revActive", "revInactive", """ "revActive", "revInactive", """
select id, factId from cards c where type = 1 and combinedDue > :lim select id, factId from cards c where queue = 1 and due > :lim
order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff) order by due limit %d""" % self.queueLimit), lim=self.dueCutoff)
self.revQueue.reverse() self.revQueue.reverse()
# Learn more # Learn more
@ -533,8 +530,8 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
self.newAvail = self.db.scalar( self.newAvail = self.db.scalar(
self.cardLimit( self.cardLimit(
"newActive", "newInactive", "newActive", "newInactive",
"select count(*) from cards c where type = 2 " "select count(*) from cards c where queue = 2 "
"and combinedDue < :lim"), lim=self.dueCutoff) "and due < :lim"), lim=self.dueCutoff)
self.spacedCards = [] self.spacedCards = []
def _updateLearnMoreCountToday(self): def _updateLearnMoreCountToday(self):
@ -566,7 +563,7 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
def _cramPreSave(self, card, ease): def _cramPreSave(self, card, ease):
# prevent it from appearing in next queue fill # prevent it from appearing in next queue fill
card.lastInterval = self.cramLastInterval card.lastInterval = self.cramLastInterval
card.type += 6 card.type = -3
def _spaceCramCards(self, card): def _spaceCramCards(self, card):
self.spacedFacts[card.factId] = time.time() + self.newSpacing self.spacedFacts[card.factId] = time.time() + self.newSpacing
@ -634,7 +631,7 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
self.revQueue = self.db.all(self.cardLimit( self.revQueue = self.db.all(self.cardLimit(
self.activeCramTags, "", """ self.activeCramTags, "", """
select id, factId from cards c select id, factId from cards c
where type between 0 and 2 where queue between 0 and 2
order by %s order by %s
limit %s""" % (self.cramOrder, self.queueLimit))) limit %s""" % (self.cramOrder, self.queueLimit)))
self.revQueue.reverse() self.revQueue.reverse()
@ -642,7 +639,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
def _rebuildCramCount(self): def _rebuildCramCount(self):
self.revCount = self.db.scalar(self.cardLimit( self.revCount = self.db.scalar(self.cardLimit(
self.activeCramTags, "", self.activeCramTags, "",
"select count(*) from cards c where type between 0 and 2")) "select count(*) from cards c where queue between 0 and 2"))
def _rebuildFailedCramCount(self): def _rebuildFailedCramCount(self):
self.failedSoonCount = len(self.failedCramQueue) self.failedSoonCount = len(self.failedCramQueue)
@ -754,7 +751,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
if not card.fromDB(self.db, id): if not card.fromDB(self.db, id):
return return
card.deck = self card.deck = self
card.genFuzz() #card.genFuzz()
card.startTimer() card.startTimer()
return card return card
@ -768,7 +765,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
# old state # old state
oldState = self.cardState(card) oldState = self.cardState(card)
oldQueue = self.cardQueue(card) oldQueue = self.cardQueue(card)
lastDelaySecs = time.time() - card.combinedDue lastDelaySecs = time.time() - card.due
lastDelay = lastDelaySecs / 86400.0 lastDelay = lastDelaySecs / 86400.0
oldSuc = card.successive oldSuc = card.successive
# update card details # update card details
@ -779,8 +776,6 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
# only update if card was not new # only update if card was not new
card.lastDue = card.due card.lastDue = card.due
card.due = self.nextDue(card, ease, oldState) card.due = self.nextDue(card, ease, oldState)
card.isDue = 0
card.lastFactor = card.factor
card.spaceUntil = 0 card.spaceUntil = 0
if not self.finishScheduler: if not self.finishScheduler:
# don't update factor in custom schedulers # don't update factor in custom schedulers
@ -798,17 +793,17 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
else: else:
self.newAvail -= 1 self.newAvail -= 1
# card stats # card stats
anki.cards.Card.updateStats(card, ease, oldState) self.updateCardStats(card, ease, oldState)
# update type & ensure past cutoff # update type & ensure past cutoff
card.type = self.cardType(card) card.type = self.cardType(card)
card.relativeDelay = card.type card.queue = card.type
if ease != 1: if ease != 1:
card.due = max(card.due, self.dueCutoff+1) card.due = max(card.due, self.dueCutoff+1)
# allow custom schedulers to munge the card # allow custom schedulers to munge the card
if self.answerPreSave: if self.answerPreSave:
self.answerPreSave(card, ease) self.answerPreSave(card, ease)
# save # save
card.combinedDue = card.due card.due = card.due
card.toDB(self.db) card.toDB(self.db)
# review history # review history
print "make sure flags is set correctly when reviewing early" print "make sure flags is set correctly when reviewing early"
@ -824,28 +819,39 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
runHook("cardAnswered", card.id, isLeech) runHook("cardAnswered", card.id, isLeech)
self.setUndoEnd(undoName) 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): def _spaceCards(self, card):
new = time.time() + self.newSpacing new = time.time() + self.newSpacing
self.db.statement(""" self.db.statement("""
update cards set update cards set
combinedDue = (case due = (case
when type = 1 then combinedDue + 86400 * (case when queue = 1 then due + 86400 * (case
when interval*:rev < 1 then 0 when interval*:rev < 1 then 0
else interval*:rev else interval*:rev
end) end)
when type = 2 then :new when queue = 2 then :new
end), end),
modified = :now, isDue = 0 modified = :now
where id != :id and factId = :factId where id != :id and factId = :factId
and combinedDue < :cut and due < :cut
and type between 1 and 2""", and queue between 1 and 2""",
id=card.id, now=time.time(), factId=card.factId, id=card.id, now=time.time(), factId=card.factId,
cut=self.dueCutoff, new=new, rev=self.revSpacing) cut=self.dueCutoff, new=new, rev=self.revSpacing)
# update local cache of seen facts # update local cache of seen facts
self.spacedFacts[card.factId] = new self.spacedFacts[card.factId] = new
def isLeech(self, card): def isLeech(self, card):
no = card.noCount no = card.lapses
fmax = self.getInt('leechFails') fmax = self.getInt('leechFails')
if not fmax: if not fmax:
return return
@ -885,8 +891,6 @@ and type between 1 and 2""",
factor = card.factor factor = card.factor
# if cramming / reviewing early # if cramming / reviewing early
if delay < 0: if delay < 0:
# FIXME: this should recreate lastInterval from interval /
# lastFactor, or we lose delay information when reviewing early
interval = max(card.lastInterval, card.interval + delay) interval = max(card.lastInterval, card.interval + delay)
if interval < self.midIntervalMin: if interval < self.midIntervalMin:
interval = 0 interval = 0
@ -950,7 +954,7 @@ and type between 1 and 2""",
def updateFactor(self, card, ease): def updateFactor(self, card, ease):
"Update CARD's factor based on EASE." "Update CARD's factor based on EASE."
card.lastFactor = card.factor print "update cardIsBeingLearnt()"
if not card.reps: if not card.reps:
# card is new, inherit beginning factor # card is new, inherit beginning factor
card.factor = self.averageFactor card.factor = self.averageFactor
@ -967,24 +971,26 @@ and type between 1 and 2""",
"Return an adjusted delay value for CARD based on EASE." "Return an adjusted delay value for CARD based on EASE."
if self.cardIsNew(card): if self.cardIsNew(card):
return 0 return 0
if card.reps and not card.successive: if card.due <= time.time():
return 0 return (time.time() - card.due) / 86400.0
if card.combinedDue <= self.dueCutoff:
return (self.dueCutoff - card.due) / 86400.0
else: else:
return (self.dueCutoff - card.combinedDue) / 86400.0 return (time.time() - card.due) / 86400.0
def resetCards(self, ids): def resetCards(self, ids=None):
"Reset progress on cards in IDS." "Reset progress on cards in IDS."
self.db.statement(""" print "position in resetCards()"
update cards set interval = :new, lastInterval = 0, lastDue = 0, sql = """
factor = 2.5, reps = 0, successive = 0, averageTime = 0, reviewTime = 0, update cards set modified=:now, position=0, type=2, queue=2, lastInterval=0,
youngEase0 = 0, youngEase1 = 0, youngEase2 = 0, youngEase3 = 0, interval=0, due=created, factor=2.5, reps=0, successive=0, lapses=0, flags=0"""
youngEase4 = 0, matureEase0 = 0, matureEase1 = 0, matureEase2 = 0, sql2 = "delete from revlog"
matureEase3 = 0,matureEase4 = 0, yesCount = 0, noCount = 0, if ids is None:
spaceUntil = 0, type = 2, relativeDelay = 2, lim = ""
combinedDue = created, modified = :now, due = created, isDue = 0 else:
where id in %s""" % ids2str(ids), now=time.time(), new=0) sids = ids2str(ids)
sql += " where id in "+sids
sql2 += " where cardId in "+sids
self.db.statement(sql, now=time.time())
self.db.statement(sql2)
if self.newCardOrder == NEW_CARDS_RANDOM: if self.newCardOrder == NEW_CARDS_RANDOM:
# we need to re-randomize now # we need to re-randomize now
self.randomizeNewCards(ids) self.randomizeNewCards(ids)
@ -1004,19 +1010,17 @@ where id in %s""" % ids2str(ids), now=time.time(), new=0)
self.db.statements(""" self.db.statements("""
update cards update cards
set due = :rand + ordinal, set due = :rand + ordinal,
combinedDue = :rand + ordinal,
modified = :now modified = :now
where factId = :fid where factId = :fid
and relativeDelay = 2""", data) and type = 2""", data)
def orderNewCards(self): def orderNewCards(self):
"Set 'due' to card creation time." "Set 'due' to card creation time."
self.db.statement(""" self.db.statement("""
update cards set update cards set
due = created, due = created,
combinedDue = created,
modified = :now modified = :now
where relativeDelay = 2""", now=time.time()) where type = 2""", now=time.time())
def rescheduleCards(self, ids, min, max): def rescheduleCards(self, ids, min, max):
"Reset cards and schedule with new interval in days (min, max)." "Reset cards and schedule with new interval in days (min, max)."
@ -1034,14 +1038,12 @@ where relativeDelay = 2""", now=time.time())
update cards set update cards set
interval = :int, interval = :int,
due = :due, due = :due,
combinedDue = :due,
reps = 1, reps = 1,
successive = 1, successive = 1,
yesCount = 1, yesCount = 1,
firstAnswered = :t, firstAnswered = :t,
queue = 1,
type = 1, type = 1,
relativeDelay = 1,
isDue = 0
where id = :id""", vals) where id = :id""", vals)
self.flushMod() self.flushMod()
@ -1079,12 +1081,12 @@ This may be in the past if the deck is not finished.
If the deck has no (enabled) cards, return None. If the deck has no (enabled) cards, return None.
Ignore new cards.""" Ignore new cards."""
earliestRev = self.db.scalar(self.cardLimit("revActive", "revInactive", """ earliestRev = self.db.scalar(self.cardLimit("revActive", "revInactive", """
select combinedDue from cards c where type = 1 select due from cards c where queue = 1
order by combinedDue order by due
limit 1""")) limit 1"""))
earliestFail = self.db.scalar(self.cardLimit("revActive", "revInactive", """ earliestFail = self.db.scalar(self.cardLimit("revActive", "revInactive", """
select combinedDue+%d from cards c where type = 0 select due+%d from cards c where queue = 0
order by combinedDue order by due
limit 1""" % self.delay0)) limit 1""" % self.delay0))
if earliestRev and earliestFail: if earliestRev and earliestFail:
return min(earliestRev, earliestFail) return min(earliestRev, earliestFail)
@ -1107,16 +1109,16 @@ limit 1""" % self.delay0))
return self.db.scalar( return self.db.scalar(
self.cardLimit( self.cardLimit(
"revActive", "revInactive", "revActive", "revInactive",
"select count(*) from cards c where type between 0 and 1 " "select count(*) from cards c where queue between 0 and 1 "
"and combinedDue < :lim"), lim=time) "and due < :lim"), lim=time)
def newCardsDueBy(self, time): def newCardsDueBy(self, time):
"Number of new cards due at TIME." "Number of new cards due at TIME."
return self.db.scalar( return self.db.scalar(
self.cardLimit( self.cardLimit(
"newActive", "newInactive", "newActive", "newInactive",
"select count(*) from cards c where type = 2 " "select count(*) from cards c where queue = 2 "
"and combinedDue < :lim"), lim=time) "and due < :lim"), lim=time)
def deckFinishedMsg(self): def deckFinishedMsg(self):
spaceSusp = "" spaceSusp = ""
@ -1151,9 +1153,8 @@ limit 1""" % self.delay0))
self.startProgress() self.startProgress()
self.db.statement(""" self.db.statement("""
update cards update cards
set type = relativeDelay - 3, set queue = -1, modified = :t
modified = :t where id in %s""" % ids2str(ids), t=time.time())
where type >= 0 and id in %s""" % ids2str(ids), t=time.time())
self.flushMod() self.flushMod()
self.finishProgress() self.finishProgress()
@ -1161,8 +1162,8 @@ where type >= 0 and id in %s""" % ids2str(ids), t=time.time())
"Unsuspend cards. Caller must .reset()" "Unsuspend cards. Caller must .reset()"
self.startProgress() self.startProgress()
self.db.statement(""" self.db.statement("""
update cards set type = relativeDelay, modified=:t update cards set queue = type, modified=:t
where type between -3 and -1 and id in %s""" % where queue = -1 and id in %s""" %
ids2str(ids), t=time.time()) ids2str(ids), t=time.time())
self.flushMod() self.flushMod()
self.finishProgress() self.finishProgress()
@ -1170,8 +1171,8 @@ where type between -3 and -1 and id in %s""" %
def buryFact(self, fact): def buryFact(self, fact):
"Bury all cards for fact until next session. Caller must .reset()" "Bury all cards for fact until next session. Caller must .reset()"
for card in fact.cards: for card in fact.cards:
if card.type in (0,1,2): if card.queue in (0,1,2):
card.type += 3 card.queue = -2
self.flushMod() self.flushMod()
# Counts # Counts
@ -1180,14 +1181,16 @@ where type between -3 and -1 and id in %s""" %
def hiddenCards(self): def hiddenCards(self):
"Assumes queue finished. True if some due cards have not been shown." "Assumes queue finished. True if some due cards have not been shown."
return self.db.scalar(""" return self.db.scalar("""
select 1 from cards where combinedDue < :now select 1 from cards where due < :now
and type between 0 and 1 limit 1""", now=self.dueCutoff) and queue between 0 and 1 limit 1""", now=self.dueCutoff)
def spacedCardCount(self): def spacedCardCount(self):
"Number of spaced cards." "Number of spaced cards."
print "spacedCardCount"
return 0
return self.db.scalar(""" return self.db.scalar("""
select count(cards.id) from cards where select count(cards.id) from cards where
combinedDue > :now and due < :now""", now=time.time()) due > :now and due < :now""", now=time.time())
def isEmpty(self): def isEmpty(self):
return not self.cardCount return not self.cardCount
@ -1205,11 +1208,11 @@ combinedDue > :now and due < :now""", now=time.time())
def newCountAll(self): def newCountAll(self):
"All new cards, including spaced." "All new cards, including spaced."
return self.db.scalar( return self.db.scalar(
"select count(id) from cards where relativeDelay = 2") "select count(id) from cards where type = 2")
def seenCardCount(self): def seenCardCount(self):
return self.db.scalar( return self.db.scalar(
"select count(id) from cards where relativeDelay between 0 and 1") "select count(id) from cards where type between 0 and 1")
# Card predicates # Card predicates
########################################################################## ##########################################################################
@ -1225,10 +1228,6 @@ combinedDue > :now and due < :now""", now=time.time())
"True if a card has never been seen before." "True if a card has never been seen before."
return card.reps == 0 return card.reps == 0
def cardIsBeingLearnt(self, card):
"True if card should use present intervals."
return card.lastInterval < 7
def cardIsYoung(self, card): def cardIsYoung(self, card):
"True if card is not new and not mature." "True if card is not new and not mature."
return (not self.cardIsNew(card) and return (not self.cardIsNew(card) and
@ -1290,7 +1289,6 @@ combinedDue > :now and due < :now""", now=time.time())
card = anki.cards.Card(fact, cardModel, created) card = anki.cards.Card(fact, cardModel, created)
if isRandom: if isRandom:
card.due = due card.due = due
card.combinedDue = due
self.flushMod() self.flushMod()
cards.append(card) cards.append(card)
# update card q/a # update card q/a
@ -1981,6 +1979,13 @@ and cards.factId = facts.id""")
select id, tags from facts select id, tags from facts
where id in %s""" % ids2str(ids)) where id in %s""" % ids2str(ids))
def cardHasTag(self, card, tag):
id = tagId(self.db, tag, create=False)
if id:
return self.db.scalar(
"select 1 from cardTags where cardId = :c and tagId = :t",
c=card.id, t=id)
# Tags: caching # Tags: caching
########################################################################## ##########################################################################
@ -2542,20 +2547,21 @@ select cardId from cardTags where cardTags.tagId in %s""" % ids2str(ids)
n = 0 n = 0
qquery += "select id from cards where type = %d" % n qquery += "select id from cards where type = %d" % n
elif token == "delayed": elif token == "delayed":
print "delayed"
qquery += ("select id from cards where " qquery += ("select id from cards where "
"due < %d and combinedDue > %d and " "due < %d and due > %d and "
"type in (0,1,2)") % ( "type in (0,1,2)") % (
self.dueCutoff, self.dueCutoff) self.dueCutoff, self.dueCutoff)
elif token == "suspended": elif token == "suspended":
qquery += ("select id from cards where " qquery += ("select id from cards where "
"type between -3 and -1") "queue = -1")
elif token == "leech": elif token == "leech":
qquery += ( qquery += (
"select id from cards where noCount >= (select value " "select id from cards where noCount >= (select value "
"from deckvars where key = 'leechFails')") "from deckvars where key = 'leechFails')")
else: # due else: # due
qquery += ("select id from cards where " qquery += ("select id from cards where "
"type in (0,1) and combinedDue < %d") % self.dueCutoff "queue between 0 and 1 and due < %d") % self.dueCutoff
elif type == SEARCH_FID: elif type == SEARCH_FID:
if fidquery: if fidquery:
if isNeg: if isNeg:
@ -3436,17 +3442,15 @@ seq > :s and seq <= :e order by seq desc""", s=start, e=end)
def updateDynamicIndices(self): def updateDynamicIndices(self):
indices = { indices = {
'intervalDesc': 'intervalDesc':
'(type, interval desc, factId, combinedDue)', '(queue, interval desc, factId, due)',
'intervalAsc': 'intervalAsc':
'(type, interval, factId, combinedDue)', '(queue, interval, factId, due)',
'randomOrder': 'randomOrder':
'(type, factId, ordinal, combinedDue)', '(queue, factId, ordinal, due)',
# new cards are sorted by due, not combinedDue, so that even if
# they are spaced, they retain their original sort order
'dueAsc': 'dueAsc':
'(type, due, factId, combinedDue)', '(queue, position, factId, due)',
'dueDesc': 'dueDesc':
'(type, due desc, factId, combinedDue)', '(queue, position desc, factId, due)',
} }
# determine required # determine required
required = [] required = []
@ -3532,7 +3536,7 @@ class DeckStorage(object):
metadata.create_all(engine) metadata.create_all(engine)
deck = DeckStorage._init(s) deck = DeckStorage._init(s)
else: else:
ver = upgradeSchema(s) ver = upgradeSchema(engine, s)
# add any possibly new tables if we're upgrading # add any possibly new tables if we're upgrading
if ver < DECK_VERSION: if ver < DECK_VERSION:
metadata.create_all(engine) metadata.create_all(engine)
@ -3601,7 +3605,7 @@ class DeckStorage(object):
deck.delay1 = 0 deck.delay1 = 0
# unsuspend buried/rev early # unsuspend buried/rev early
deck.db.statement( deck.db.statement(
"update cards set type = relativeDelay where type > 2") "update cards set queue = type where queue between -3 and -2")
deck.db.commit() deck.db.commit()
# check if deck has been moved, and disable syncing # check if deck has been moved, and disable syncing
deck.checkSyncHash() deck.checkSyncHash()

View file

@ -94,38 +94,7 @@ class AnkiExporter(Exporter):
res = server.applyPayload(payload) res = server.applyPayload(payload)
if not self.includeSchedulingInfo: if not self.includeSchedulingInfo:
self.deck.updateProgress() self.deck.updateProgress()
self.newDeck.db.statement(""" self.newDeck.resetCards()
delete from revlog""")
self.newDeck.db.statement("""
update cards set
interval = 0,
lastInterval = 0,
due = created,
lastDue = 0,
factor = 2.5,
firstAnswered = 0,
reps = 0,
successive = 0,
averageTime = 0,
reviewTime = 0,
youngEase0 = 0,
youngEase1 = 0,
youngEase2 = 0,
youngEase3 = 0,
youngEase4 = 0,
matureEase0 = 0,
matureEase1 = 0,
matureEase2 = 0,
matureEase3 = 0,
matureEase4 = 0,
yesCount = 0,
noCount = 0,
spaceUntil = 0,
type = 2,
relativeDelay = 2,
combinedDue = created,
modified = :now
""", now=time.time())
# media # media
if self.includeMedia: if self.includeMedia:
server.deck.mediaPrefix = "" server.deck.mediaPrefix = ""

View file

@ -65,11 +65,11 @@ class DeckGraphs(object):
self.endOfDay = self.deck.failedCutoff self.endOfDay = self.deck.failedCutoff
t = time.time() t = time.time()
young = """ young = """
select interval, combinedDue from cards c select interval, due from cards c
where relativeDelay between 0 and 1 and type >= 0 and interval <= 21""" where queue between 0 and 1 and interval <= 21"""
mature = """ mature = """
select interval, combinedDue select interval, due
from cards c where relativeDelay = 1 and type >= 0 and interval > 21""" from cards c where queue = 1 and interval > 21"""
if self.selective: if self.selective:
young = self.deck._cardLimit("revActive", "revInactive", young = self.deck._cardLimit("revActive", "revInactive",
young) young)
@ -238,8 +238,13 @@ group by day order by day
days = {} days = {}
fig = Figure(figsize=(self.width, self.height), dpi=self.dpi) fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
limit = self.endOfDay - (numdays) * 86400 limit = self.endOfDay - (numdays) * 86400
res = self.deck.db.column0("select %s from cards where %s >= %f" % if attr == "created":
(attr, attr, limit)) res = self.deck.db.column0("select %s from cards where %s >= %f" %
(attr, attr, limit))
else:
# firstAnswered
res = self.deck.db.column0(
"select time from revlog where rep = 1")
for r in res: for r in res:
d = int((r - self.endOfDay) / 86400.0) d = int((r - self.endOfDay) / 86400.0)
days[d] = days.get(d, 0) + 1 days[d] = days.get(d, 0) + 1

View file

@ -307,7 +307,7 @@ where factId in (%s)""" % ",".join([str(s) for s in factIds]))
else: else:
t = 2 t = 2
data['type'] = t data['type'] = t
data['relativeDelay'] = t data['queue'] = t
return data return data
def stripInvalid(self, cards): def stripInvalid(self, cards):

View file

@ -28,5 +28,5 @@ insert into revlog values (
:userTime, :flags)""", :userTime, :flags)""",
created=time.time(), cardId=card.id, ease=ease, rep=card.reps, created=time.time(), cardId=card.id, ease=ease, rep=card.reps,
lastInterval=card.lastInterval, interval=card.interval, lastInterval=card.lastInterval, interval=card.interval,
factor=card.factor, userTime=card.thinkingTime(), factor=card.factor, userTime=card.userTime(),
flags=flags) flags=flags)

View file

@ -24,11 +24,13 @@ class CardStats(object):
fmtFloat = anki.utils.fmtFloat fmtFloat = anki.utils.fmtFloat
self.txt = "<table>" self.txt = "<table>"
self.addLine(_("Added"), self.strTime(c.created)) self.addLine(_("Added"), self.strTime(c.created))
if c.firstAnswered: first = self.deck.db.scalar(
self.addLine(_("First Review"), self.strTime(c.firstAnswered)) "select time from revlog where rep = 1 and cardId = :id", id=c.id)
if first:
self.addLine(_("First Review"), self.strTime(first))
self.addLine(_("Changed"), self.strTime(c.modified)) self.addLine(_("Changed"), self.strTime(c.modified))
if c.reps: if c.reps:
next = time.time() - c.combinedDue next = time.time() - c.due
if next > 0: if next > 0:
next = _("%s ago") % fmt(next) next = _("%s ago") % fmt(next)
else: else:
@ -36,20 +38,14 @@ class CardStats(object):
self.addLine(_("Due"), next) self.addLine(_("Due"), next)
self.addLine(_("Interval"), fmt(c.interval * 86400)) self.addLine(_("Interval"), fmt(c.interval * 86400))
self.addLine(_("Ease"), fmtFloat(c.factor, point=2)) self.addLine(_("Ease"), fmtFloat(c.factor, point=2))
if c.lastDue:
last = _("%s ago") % fmt(time.time() - c.lastDue)
self.addLine(_("Last Due"), last)
if c.interval != c.lastInterval:
# don't show the last interval if it hasn't been updated yet
self.addLine(_("Last Interval"), fmt(c.lastInterval * 86400))
self.addLine(_("Last Ease"), fmtFloat(c.lastFactor, point=2))
if c.reps: if c.reps:
self.addLine(_("Reviews"), "%d/%d (s=%d)" % ( self.addLine(_("Reviews"), "%d/%d (s=%d)" % (
c.yesCount, c.reps, c.successive)) c.reps-c.lapses, c.reps, c.successive))
avg = fmt(c.averageTime, point=2) (cnt, total) = self.deck.db.first(
self.addLine(_("Average Time"),avg) "select count(), sum(userTime) from revlog where cardId = :id", id=c.id)
total = fmt(c.reviewTime, point=2) if cnt:
self.addLine(_("Total Time"), total) self.addLine(_("Average Time"), fmt(total / float(cnt), point=2))
self.addLine(_("Total Time"), fmt(total, point=2))
self.addLine(_("Model Tags"), c.fact.model.tags) self.addLine(_("Model Tags"), c.fact.model.tags)
self.addLine(_("Card Template") + "&nbsp;"*5, c.cardModel.name) self.addLine(_("Card Template") + "&nbsp;"*5, c.cardModel.name)
self.txt += "</table>" self.txt += "</table>"
@ -244,10 +240,10 @@ class DeckStats(object):
return (all, yes, yes/float(all)*100) return (all, yes, yes/float(all)*100)
def getYoungCorrect(self): def getYoungCorrect(self):
return self.getMatureCorrect("lastInterval <= 21 and reps != 1") return self.getMatureCorrect("lastInterval <= 21 and rep != 1")
def getNewCorrect(self): def getNewCorrect(self):
return self.getMatureCorrect("reps = 1") return self.getMatureCorrect("rep = 1")
def getDaysReviewed(self, start, finish): def getDaysReviewed(self, start, finish):
today = self.deck.failedCutoff today = self.deck.failedCutoff
@ -308,14 +304,14 @@ where time >= :x and time <= :y""",x=x,y=y, off=self.deck.utcOffset)
return self.deck.db.scalar( return self.deck.db.scalar(
"select sum(1/round(max(interval, 1)+0.5)) from cards " "select sum(1/round(max(interval, 1)+0.5)) from cards "
"where cards.reps > 0 " "where cards.reps > 0 "
"and type >= 0") or 0 "and queue != -1") or 0
def getWorkloadPeriod(self, period): def getWorkloadPeriod(self, period):
cutoff = time.time() + 86400 * period cutoff = time.time() + 86400 * period
return (self.deck.db.scalar(""" return (self.deck.db.scalar("""
select count(id) from cards select count(id) from cards
where combinedDue < :cutoff where due < :cutoff
and type >= 0 and relativeDelay in (0,1)""", cutoff=cutoff) or 0) / float(period) and queue != -1 and type between 0 and 1""", cutoff=cutoff) or 0) / float(period)
def getPastWorkloadPeriod(self, period): def getPastWorkloadPeriod(self, period):
cutoff = time.time() - 86400 * period cutoff = time.time() - 86400 * period

View file

@ -2,12 +2,13 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
DECK_VERSION = 74 DECK_VERSION = 75
from anki.db import *
from anki.lang import _ from anki.lang import _
from anki.media import rebuildMediaDir from anki.media import rebuildMediaDir
def upgradeSchema(s): def upgradeSchema(engine, s):
"Alter tables prior to ORM initialization." "Alter tables prior to ORM initialization."
ver = s.scalar("select version from decks limit 1") ver = s.scalar("select version from decks limit 1")
# add a checksum to fields # add a checksum to fields
@ -18,19 +19,36 @@ def upgradeSchema(s):
"not null default ''") "not null default ''")
except: except:
pass pass
if ver < 75:
# copy cards into new temporary table
sql = s.scalar(
"select sql from sqlite_master where name = 'cards'")
sql = sql.replace("TABLE cards", "temporary table cards2")
s.execute(sql)
s.execute("insert into cards2 select * from cards")
# drop the old cards table and create the new one
s.execute("drop table cards")
import cards
metadata.create_all(engine, tables=[cards.cardsTable])
# move data across and delete temp table
s.execute("""
insert into cards select id, factId, cardModelId, created, modified,
question, answer, 0, ordinal, 0, relativeDelay, type, lastInterval, interval,
due, factor, reps, successive, noCount from cards2""")
s.execute("drop table cards2")
return ver return ver
def updateIndices(deck): def updateIndices(deck):
"Add indices to the DB." "Add indices to the DB."
# counts, failed cards # due counts, failed card queue
deck.db.statement(""" deck.db.statement("""
create index if not exists ix_cards_typeCombined on cards create index if not exists ix_cards_queueDue on cards
(type, combinedDue, factId)""") (queue, due, factId)""")
# scheduler-agnostic type # counting cards of a given type
deck.db.statement(""" deck.db.statement("""
create index if not exists ix_cards_relativeDelay on cards create index if not exists ix_cards_type on cards
(relativeDelay)""") (type)""")
# index on modified, to speed up sync summaries # sync summaries
deck.db.statement(""" deck.db.statement("""
create index if not exists ix_cards_modified on cards create index if not exists ix_cards_modified on cards
(modified)""") (modified)""")
@ -88,114 +106,8 @@ def upgradeDeck(deck):
oldmod = deck.modified oldmod = deck.modified
else: else:
prog = False prog = False
if deck.version < 43:
raise Exception("oldDeckVersion")
if deck.version < 44:
# leaner indices
deck.db.statement("drop index if exists ix_cards_factId")
deck.version = 44
deck.db.commit()
if deck.version < 48:
deck.updateFieldCache(deck.db.column0("select id from facts"))
deck.version = 48
deck.db.commit()
if deck.version < 52:
dname = deck.name()
sname = deck.syncName
if sname and dname != sname:
deck.notify(_("""\
When syncing, Anki now uses the same deck name on the server as the deck \
name on your computer. Because you had '%(dname)s' set to sync to \
'%(sname)s' on the server, syncing has been temporarily disabled.
If you want to keep your changes to the online version, please use \
File>Download>Personal Deck to download the online version.
If you want to keep the version on your computer, please enable \
syncing again via Settings>Deck Properties>Synchronisation.
If you have syncing disabled in the preferences, you can ignore \
this message. (ERR-0101)""") % {
'sname':sname, 'dname':dname})
deck.disableSyncing()
elif sname:
deck.enableSyncing()
deck.version = 52
deck.db.commit()
if deck.version < 53:
if deck.getBool("perDay"):
if deck.hardIntervalMin == 0.333:
deck.hardIntervalMin = max(1.0, deck.hardIntervalMin)
deck.hardIntervalMax = max(1.1, deck.hardIntervalMax)
deck.version = 53
deck.db.commit()
if deck.version < 54:
# broken versions of the DB orm die if this is a bool with a
# non-int value
deck.db.statement("update fieldModels set editFontFamily = 1");
deck.version = 54
deck.db.commit()
if deck.version < 61:
# do our best to upgrade templates to the new style
txt = '''\
<span style="font-family: %s; font-size: %spx; color: %s; white-space: pre-wrap;">%s</span>'''
for m in deck.models:
unstyled = []
for fm in m.fieldModels:
# find which fields had explicit formatting
if fm.quizFontFamily or fm.quizFontSize or fm.quizFontColour:
pass
else:
unstyled.append(fm.name)
# fill out missing info
fm.quizFontFamily = fm.quizFontFamily or u"Arial"
fm.quizFontSize = fm.quizFontSize or 20
fm.quizFontColour = fm.quizFontColour or "#000000"
fm.editFontSize = fm.editFontSize or 20
unstyled = set(unstyled)
for cm in m.cardModels:
# embed the old font information into card templates
cm.qformat = txt % (
cm.questionFontFamily,
cm.questionFontSize,
cm.questionFontColour,
cm.qformat)
cm.aformat = txt % (
cm.answerFontFamily,
cm.answerFontSize,
cm.answerFontColour,
cm.aformat)
# escape fields that had no previous styling
for un in unstyled:
cm.qformat = cm.qformat.replace("%("+un+")s", "{{{%s}}}"%un)
cm.aformat = cm.aformat.replace("%("+un+")s", "{{{%s}}}"%un)
# rebuild q/a for the above & because latex has changed
for m in deck.models:
deck.updateCardsFromModel(m, dirty=False)
# rebuild the media db based on new format
rebuildMediaDir(deck, dirty=False)
deck.version = 61
deck.db.commit()
if deck.version < 62:
# updated indices
deck.db.statement("drop index if exists ix_cards_typeCombined")
updateIndices(deck)
deck.version = 62
deck.db.commit()
if deck.version < 64:
# remove old static indices, as all clients should be libanki1.2+
for d in ("ix_cards_duePriority",
"ix_cards_priorityDue"):
deck.db.statement("drop index if exists %s" % d)
deck.version = 64
deck.db.commit()
# note: we keep the priority index for now
if deck.version < 65: if deck.version < 65:
# we weren't correctly setting relativeDelay when answering cards raise Exception("oldDeckVersion")
# in previous versions, so ensure everything is set correctly
deck.rebuildTypes()
deck.version = 65
deck.db.commit()
# skip a few to allow for updates to stable tree # skip a few to allow for updates to stable tree
if deck.version < 70: if deck.version < 70:
# update dynamic indices given we don't use priority anymore # update dynamic indices given we don't use priority anymore
@ -228,7 +140,6 @@ this message. (ERR-0101)""") % {
deck.db.commit() deck.db.commit()
if deck.version < 73: if deck.version < 73:
# remove stats, as it's all in the revlog now # remove stats, as it's all in the revlog now
deck.db.statement("drop index if exists ix_stats_typeDay")
deck.db.statement("drop table if exists stats") deck.db.statement("drop table if exists stats")
deck.version = 73 deck.version = 73
deck.db.commit() deck.db.commit()
@ -245,7 +156,15 @@ min(thinkingTime, 60), 0 from reviewHistory""")
deck.db.statement("drop index if exists ix_cards_priority") deck.db.statement("drop index if exists ix_cards_priority")
deck.version = 74 deck.version = 74
deck.db.commit() deck.db.commit()
if deck.version < 75:
# suspended cards don't use ranges anymore
deck.db.execute("update cards set queue=-1 where queue between -3 and -1")
deck.db.execute("update cards set queue=-2 where queue between 3 and 5")
deck.db.execute("update cards set queue=-3 where queue between 6 and 8")
# new indices for new cards table
updateIndices(deck)
deck.version = 75
deck.db.commit()
# executing a pragma here is very slow on large decks, so we store # executing a pragma here is very slow on large decks, so we store
# our own record # our own record