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
##########################################################################
# 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(
'cards', metadata,
Column('id', Integer, primary_key=True),
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
# general
Column('created', 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('answer', UnicodeText, nullable=False, default=u""),
Column('priority', Integer, nullable=False, default=2), # obsolete
Column('interval', Float, nullable=False, default=0),
Column('flags', Integer, 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('due', Float, nullable=False, default=time.time),
Column('lastDue', Float, nullable=False, default=0),
Column('interval', Float, nullable=False, default=0),
Column('due', Float, nullable=False),
Column('factor', Float, nullable=False, default=2.5),
Column('lastFactor', Float, nullable=False, default=2.5),
Column('firstAnswered', Float, nullable=False, default=0),
# stats
# counters
Column('reps', Integer, nullable=False, default=0),
Column('successive', Integer, nullable=False, default=0),
Column('averageTime', Float, 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))
Column('lapses', Integer, nullable=False, default=0))
class Card(object):
"A card."
def __init__(self, fact=None, cardModel=None, created=None):
self.tags = u""
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()
if created:
self.created = created
self.due = created
else:
self.due = self.modified
self.combinedDue = self.due
self.position = self.due
if fact:
self.fact = fact
if cardModel:
@ -87,13 +65,27 @@ class Card(object):
# for non-orm use
self.cardModelId = cardModel.id
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):
# format qa
d = {}
for f in self.fact.model.fieldModels:
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)
# find old media references
files = {}
@ -119,25 +111,6 @@ class Card(object):
updateMediaCount(deck, f, cnt)
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):
div = '''<div class="card%s" id="cm%s%s">%s</div>''' % (
type[0], type[0], hexifyID(self.cardModelId),
@ -158,52 +131,14 @@ class Card(object):
def htmlAnswer(self, align=True):
return self.htmlQuestion(type="answer", align=align)
def updateStats(self, ease, state):
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):
def _splitTags(self):
return (self.fact.tags, self.fact.model.tags, self.cardModel.name)
def allTags(self):
"Non-canonified string of all tags."
return (self.fact.tags + "," +
self.fact.model.tags)
def hasTag(self, tag):
return findTag(tag, parseTags(self.allTags()))
# Non-ORM
##########################################################################
def fromDB(self, s, id):
r = s.first("""select
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)
r = s.first("""select * from cards where id = :id""", id=id)
if not r:
return
(self.id,
@ -211,74 +146,42 @@ from cards where id = :id""", id=id)
self.cardModelId,
self.created,
self.modified,
self.tags,
self.ordinal,
self.question,
self.answer,
self.priority,
self.interval,
self.flags,
self.ordinal,
self.position,
self.type,
self.queue,
self.lastInterval,
self.interval,
self.due,
self.lastDue,
self.factor,
self.lastFactor,
self.firstAnswered,
self.reps,
self.successive,
self.averageTime,
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
self.lapses) = r
return True
def toDB(self, s):
"Write card to DB."
s.execute("""update cards set
factId=:factId,
cardModelId=:cardModelId,
created=:created,
modified=:modified,
tags=:tags,
interval=:interval,
question=:question,
answer=:answer,
flags=:flags,
ordinal=:ordinal,
position=:position,
type=:type,
queue=:queue,
lastInterval=:lastInterval,
interval=:interval,
due=:due,
lastDue=:lastDue,
factor=:factor,
lastFactor=:lastFactor,
firstAnswered=:firstAnswered,
reps=:reps,
successive=:successive,
averageTime=:averageTime,
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
lapses=:lapses
where id=:id""", self.__dict__)
mapper(Card, cardsTable, properties={
@ -292,7 +195,6 @@ mapper(Fact, factsTable, properties={
'fields': relation(Field, backref="fact", order_by=Field.ordinal),
})
# Card deletions
##########################################################################

View file

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

View file

@ -94,38 +94,7 @@ class AnkiExporter(Exporter):
res = server.applyPayload(payload)
if not self.includeSchedulingInfo:
self.deck.updateProgress()
self.newDeck.db.statement("""
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())
self.newDeck.resetCards()
# media
if self.includeMedia:
server.deck.mediaPrefix = ""

View file

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

View file

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

View file

@ -24,11 +24,13 @@ class CardStats(object):
fmtFloat = anki.utils.fmtFloat
self.txt = "<table>"
self.addLine(_("Added"), self.strTime(c.created))
if c.firstAnswered:
self.addLine(_("First Review"), self.strTime(c.firstAnswered))
first = self.deck.db.scalar(
"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))
if c.reps:
next = time.time() - c.combinedDue
next = time.time() - c.due
if next > 0:
next = _("%s ago") % fmt(next)
else:
@ -36,20 +38,14 @@ class CardStats(object):
self.addLine(_("Due"), next)
self.addLine(_("Interval"), fmt(c.interval * 86400))
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:
self.addLine(_("Reviews"), "%d/%d (s=%d)" % (
c.yesCount, c.reps, c.successive))
avg = fmt(c.averageTime, point=2)
self.addLine(_("Average Time"),avg)
total = fmt(c.reviewTime, point=2)
self.addLine(_("Total Time"), total)
c.reps-c.lapses, c.reps, c.successive))
(cnt, total) = self.deck.db.first(
"select count(), sum(userTime) from revlog where cardId = :id", id=c.id)
if cnt:
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(_("Card Template") + "&nbsp;"*5, c.cardModel.name)
self.txt += "</table>"
@ -244,10 +240,10 @@ class DeckStats(object):
return (all, yes, yes/float(all)*100)
def getYoungCorrect(self):
return self.getMatureCorrect("lastInterval <= 21 and reps != 1")
return self.getMatureCorrect("lastInterval <= 21 and rep != 1")
def getNewCorrect(self):
return self.getMatureCorrect("reps = 1")
return self.getMatureCorrect("rep = 1")
def getDaysReviewed(self, start, finish):
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(
"select sum(1/round(max(interval, 1)+0.5)) from cards "
"where cards.reps > 0 "
"and type >= 0") or 0
"and queue != -1") or 0
def getWorkloadPeriod(self, period):
cutoff = time.time() + 86400 * period
return (self.deck.db.scalar("""
select count(id) from cards
where combinedDue < :cutoff
and type >= 0 and relativeDelay in (0,1)""", cutoff=cutoff) or 0) / float(period)
where due < :cutoff
and queue != -1 and type between 0 and 1""", cutoff=cutoff) or 0) / float(period)
def getPastWorkloadPeriod(self, period):
cutoff = time.time() - 86400 * period

View file

@ -2,12 +2,13 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# 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.media import rebuildMediaDir
def upgradeSchema(s):
def upgradeSchema(engine, s):
"Alter tables prior to ORM initialization."
ver = s.scalar("select version from decks limit 1")
# add a checksum to fields
@ -18,19 +19,36 @@ def upgradeSchema(s):
"not null default ''")
except:
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
def updateIndices(deck):
"Add indices to the DB."
# counts, failed cards
# due counts, failed card queue
deck.db.statement("""
create index if not exists ix_cards_typeCombined on cards
(type, combinedDue, factId)""")
# scheduler-agnostic type
create index if not exists ix_cards_queueDue on cards
(queue, due, factId)""")
# counting cards of a given type
deck.db.statement("""
create index if not exists ix_cards_relativeDelay on cards
(relativeDelay)""")
# index on modified, to speed up sync summaries
create index if not exists ix_cards_type on cards
(type)""")
# sync summaries
deck.db.statement("""
create index if not exists ix_cards_modified on cards
(modified)""")
@ -88,114 +106,8 @@ def upgradeDeck(deck):
oldmod = deck.modified
else:
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:
# we weren't correctly setting relativeDelay when answering cards
# in previous versions, so ensure everything is set correctly
deck.rebuildTypes()
deck.version = 65
deck.db.commit()
raise Exception("oldDeckVersion")
# skip a few to allow for updates to stable tree
if deck.version < 70:
# update dynamic indices given we don't use priority anymore
@ -228,7 +140,6 @@ this message. (ERR-0101)""") % {
deck.db.commit()
if deck.version < 73:
# 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.version = 73
deck.db.commit()
@ -245,7 +156,15 @@ min(thinkingTime, 60), 0 from reviewHistory""")
deck.db.statement("drop index if exists ix_cards_priority")
deck.version = 74
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
# our own record