This commit is contained in:
Damien Elmes 2008-11-07 18:44:49 +09:00
parent 83bc433e19
commit b2d0e5d3df
16 changed files with 393 additions and 643 deletions

View file

@ -10,9 +10,9 @@ __docformat__ = 'restructuredtext'
import time, sys, math, random import time, sys, math, random
from anki.db import * from anki.db import *
from anki.models import CardModel, Model, FieldModel from anki.models import CardModel, Model, FieldModel, formatQA
from anki.facts import Fact, factsTable, Field from anki.facts import Fact, factsTable, Field
from anki.utils import parseTags, findTag, stripHTML, genID from anki.utils import parseTags, findTag, stripHTML, genID, hexifyID
# Cards # Cards
########################################################################## ##########################################################################
@ -58,7 +58,7 @@ cardsTable = Table(
# data to the above # data to the above
Column('yesCount', Integer, nullable=False, default=0), Column('yesCount', Integer, nullable=False, default=0),
Column('noCount', Integer, nullable=False, default=0), Column('noCount', Integer, nullable=False, default=0),
# cache # caching
Column('spaceUntil', Float, nullable=False, default=0), Column('spaceUntil', Float, nullable=False, default=0),
Column('relativeDelay', Float, nullable=False, default=0), Column('relativeDelay', Float, nullable=False, default=0),
Column('isDue', Boolean, nullable=False, default=0), Column('isDue', Boolean, nullable=False, default=0),
@ -81,13 +81,12 @@ class Card(object):
if cardModel: if cardModel:
self.cardModel = cardModel self.cardModel = cardModel
self.ordinal = cardModel.ordinal self.ordinal = cardModel.ordinal
self.question = cardModel.renderQA(self, self.fact, "question") d = {}
self.answer = cardModel.renderQA(self, self.fact, "answer") for f in self.fact.model.fieldModels:
d[f.name] = (f.id, self.fact[f.name])
htmlQuestion = property(lambda self: self.cardModel.renderQA( qa = formatQA(None, fact.modelId, d, self.allTags(), cardModel)
self, self.fact, "question", format="html")) self.question = qa['question']
htmlAnswer = property(lambda self: self.cardModel.renderQA( self.answer = qa['answer']
self, self.fact, "answer", format="html"))
def setModified(self): def setModified(self):
self.modified = time.time() self.modified = time.time()
@ -104,13 +103,27 @@ class Card(object):
def totalTime(self): def totalTime(self):
return time.time() - self.timerStarted return time.time() - self.timerStarted
def css(self):
return self.cardModel.css() + self.fact.css()
def genFuzz(self): def genFuzz(self):
"Generate a random offset to spread intervals." "Generate a random offset to spread intervals."
self.fuzz = random.uniform(0.95, 1.05) self.fuzz = random.uniform(0.95, 1.05)
def htmlQuestion(self, type="question"):
div = '''<div id="cm%s%s">%s</div>''' % (
type[0], hexifyID(self.cardModel.id), getattr(self, type))
# add outer div & alignment (with tables due to qt's html handling)
attr = type + 'Align'
if getattr(self.cardModel, attr) == 0:
align = "center"
elif getattr(self.cardModel, attr) == 1:
align = "left"
else:
align = "right"
return (("<center><table width=95%%><tr><td align=%s>" % align) +
div + "</td></tr></table></center>")
def htmlAnswer(self):
return self.htmlQuestion(type="answer")
def updateStats(self, ease, state): def updateStats(self, ease, state):
self.reps += 1 self.reps += 1
if ease > 1: if ease > 1:
@ -139,12 +152,15 @@ class Card(object):
self.firstAnswered = time.time() self.firstAnswered = time.time()
self.setModified() self.setModified()
def allTags(self):
"Non-canonified string of all tags."
return (self.tags + "," +
self.fact.tags + "," +
self.cardModel.name + "," +
self.fact.model.tags)
def hasTag(self, tag): def hasTag(self, tag):
alltags = parseTags(self.tags + "," + return findTag(tag, parseTags(self.allTags()))
self.fact.tags + "," +
self.cardModel.name + "," +
self.fact.model.tags)
return findTag(tag, alltags)
def fromDB(self, s, id): def fromDB(self, s, id):
r = s.first("select * from cards where id = :id", r = s.first("select * from cards where id = :id",
@ -245,6 +261,7 @@ mapper(Fact, factsTable, properties={
cardsTable.c.id == factsTable.c.lastCardId), cardsTable.c.id == factsTable.c.lastCardId),
}) })
# Card deletions # Card deletions
########################################################################## ##########################################################################

View file

@ -72,11 +72,11 @@ class SessionHelper(object):
def statement(self, sql, **kwargs): def statement(self, sql, **kwargs):
"Execute a statement without returning any results. Flush first." "Execute a statement without returning any results. Flush first."
self.execute(text(sql), kwargs) return self.execute(text(sql), kwargs)
def statements(self, sql, data): def statements(self, sql, data):
"Execute a statement across data. Flush first." "Execute a statement across data. Flush first."
self.execute(text(sql), data) return self.execute(text(sql), data)
def __repr__(self): def __repr__(self):
return repr(self._session) return repr(self._session)

View file

@ -8,16 +8,21 @@ The Deck
""" """
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
# - rebuildqueue compuls.
import tempfile, time, os, random, sys, re, stat, shutil, types import tempfile, time, os, random, sys, re, stat, shutil, types
from anki.db import * from anki.db import *
from anki.lang import _ from anki.lang import _
from anki.errors import DeckAccessError, DeckWrongFormatError from anki.errors import DeckAccessError, DeckWrongFormatError
from anki.stdmodels import BasicModel from anki.stdmodels import BasicModel
from anki.utils import parseTags, tidyHTML, genID, ids2str from anki.utils import parseTags, tidyHTML, genID, ids2str, hexifyID, canonifyTags
from anki.history import CardHistoryEntry from anki.history import CardHistoryEntry
from anki.models import Model, CardModel from anki.models import Model, CardModel, formatQA
from anki.stats import dailyStats, globalStats, genToday from anki.stats import dailyStats, globalStats, genToday
from anki.fonts import toPlatformFont
from operator import itemgetter
from itertools import groupby
# ensure all the metadata in other files is loaded before proceeding # ensure all the metadata in other files is loaded before proceeding
import anki.models, anki.facts, anki.cards, anki.stats import anki.models, anki.facts, anki.cards, anki.stats
@ -41,7 +46,7 @@ decksTable = Table(
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('description', UnicodeText, nullable=False, default=u""), Column('description', UnicodeText, nullable=False, default=u""),
Column('version', Integer, nullable=False, default=11), Column('version', Integer, nullable=False, default=12),
Column('currentModelId', Integer, ForeignKey("models.id")), Column('currentModelId', Integer, ForeignKey("models.id")),
# syncing # syncing
Column('syncName', UnicodeText), Column('syncName', UnicodeText),
@ -78,7 +83,14 @@ decksTable = Table(
Column('sessionRepLimit', Integer, nullable=False, default=100), Column('sessionRepLimit', Integer, nullable=False, default=100),
Column('sessionTimeLimit', Integer, nullable=False, default=1800), Column('sessionTimeLimit', Integer, nullable=False, default=1800),
# stats offset # stats offset
Column('utcOffset', Float, nullable=False, default=0)) Column('utcOffset', Float, nullable=False, default=0),
# count cache
Column('cardCount', Integer, nullable=False, default=0),
Column('factCount', Integer, nullable=False, default=0),
Column('failedNowCount', Integer, nullable=False, default=0),
Column('failedSoonCount', Integer, nullable=False, default=0),
Column('revCount', Integer, nullable=False, default=0),
Column('newCount', Integer, nullable=False, default=0))
class Deck(object): class Deck(object):
"Top-level object. Manages facts, cards and scheduling information." "Top-level object. Manages facts, cards and scheduling information."
@ -98,7 +110,6 @@ class Deck(object):
def _initVars(self): def _initVars(self):
self.lastTags = u"" self.lastTags = u""
self.lastLoaded = time.time() self.lastLoaded = time.time()
self._countsDirty = True
def modifiedSinceSave(self): def modifiedSinceSave(self):
return self.modified > self.lastLoaded return self.modified > self.lastLoaded
@ -108,30 +119,20 @@ class Deck(object):
def getCard(self, orm=True): def getCard(self, orm=True):
"Return the next card object, or None." "Return the next card object, or None."
self.checkDue()
id = self.getCardId() id = self.getCardId()
if id: if id:
return self.cardFromId(id, orm) return self.cardFromId(id, orm)
def getCards(self, limit=1, orm=True):
"""Return LIMIT number of new card objects.
Caller must ensure multiple cards of the same fact are not shown."""
ids = self.getCardIds(limit)
return [self.cardFromId(x, orm) for x in ids]
def getCardId(self): def getCardId(self):
"Return the next due card id, or None." "Return the next due card id, or None."
now = time.time()
ids = []
# failed card due? # failed card due?
id = self.s.scalar("select id from failedCardsNow limit 1") if self.failedNowCount:
if id: return self.s.scalar("select id from failedCardsNow limit 1")
return id
# failed card queue too big? # failed card queue too big?
if self.failedCount >= self.failedCardMax: if self.failedSoonCount >= self.failedCardMax:
id = self.s.scalar( return self.s.scalar(
"select id from failedCardsSoon limit 1") "select id from failedCardsSoon limit 1")
if id:
return id
# distribute new cards? # distribute new cards?
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE: if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
if self._timeForNewCard(): if self._timeForNewCard():
@ -139,9 +140,8 @@ Caller must ensure multiple cards of the same fact are not shown."""
if id: if id:
return id return id
# card due for review? # card due for review?
id = self.s.scalar("select id from revCards limit 1") if self.revCount:
if id: return self.s.scalar("select id from revCards limit 1")
return id
# new card last? # new card last?
if self.newCardSpacing == NEW_CARDS_LAST: if self.newCardSpacing == NEW_CARDS_LAST:
id = self._maybeGetNewCard() id = self._maybeGetNewCard()
@ -153,21 +153,17 @@ Caller must ensure multiple cards of the same fact are not shown."""
"select id from failedCardsSoon limit 1") "select id from failedCardsSoon limit 1")
return id return id
def getCardIds(self, limit): def getCards(self):
"""Return limit number of cards. sel = """
Caller is responsible for ensuring cards are not spaced.""" select id, factId, modified, question, answer, cardModelId,
def getCard(): reps, successive from """
id = self.getCardId() if self.newCardOrder == 0:
self.s.statement("update cards set isDue = 0 where id = :id", id=id) new = "acqCardsRandom"
return id else:
arr = [] new = "acqCardsOrdered"
for i in range(limit): return {'failed': self.s.all(sel + "failedCardsNow limit 30"),
c = getCard() 'rev': self.s.all(sel + "revCards limit 30"),
if c: 'new': self.s.all(sel + new + " limit 30")}
arr.append(c)
else:
break
return arr
# Get card: helper functions # Get card: helper functions
########################################################################## ##########################################################################
@ -175,7 +171,7 @@ Caller is responsible for ensuring cards are not spaced."""
def _timeForNewCard(self): def _timeForNewCard(self):
"True if it's time to display a new card when distributing." "True if it's time to display a new card when distributing."
# no cards for review, so force new # no cards for review, so force new
if not self.reviewCount: if not self.revCount:
return True return True
# force old if there are very high priority cards # force old if there are very high priority cards
if self.s.scalar( if self.s.scalar(
@ -189,7 +185,8 @@ Caller is responsible for ensuring cards are not spaced."""
def _maybeGetNewCard(self): def _maybeGetNewCard(self):
"Get a new card, provided daily new card limit not exceeded." "Get a new card, provided daily new card limit not exceeded."
if not self.newCountLeftToday: if not self.newCountToday:
print "no count"
return return
return self._getNewCard() return self._getNewCard()
@ -221,6 +218,7 @@ Caller is responsible for ensuring cards are not spaced."""
########################################################################## ##########################################################################
def answerCard(self, card, ease): def answerCard(self, card, ease):
t = time.time()
self.checkDailyStats() self.checkDailyStats()
now = time.time() now = time.time()
oldState = self.cardState(card) oldState = self.cardState(card)
@ -233,8 +231,7 @@ Caller is responsible for ensuring cards are not spaced."""
card.isDue = 0 card.isDue = 0
card.lastFactor = card.factor card.lastFactor = card.factor
self.updateFactor(card, ease) self.updateFactor(card, ease)
# spacing - first, we get the times of all other cards with the same # spacing
# fact
(minSpacing, spaceFactor) = self.s.first(""" (minSpacing, spaceFactor) = self.s.first("""
select models.initialSpacing, models.spacing from select models.initialSpacing, models.spacing from
facts, models where facts.modelId = models.id and facts.id = :id""", id=card.factId) facts, models where facts.modelId = models.id and facts.id = :id""", id=card.factId)
@ -248,6 +245,22 @@ where factId = :fid and id != :id""", fid=card.factId, id=card.id) or 0
space = space * spaceFactor * 86400.0 space = space * spaceFactor * 86400.0
space = max(minSpacing, space) space = max(minSpacing, space)
space += time.time() space += time.time()
# check what other cards we've spaced
for (type, count) in self.s.all("""
select type, count(type) from cards
where factId = :fid and isDue = 1
group by type""", fid=card.factId):
#print type, count
if type == 0:
#print "minus failed"
self.failedNowCount -= count
elif type == 1:
#print "minus old"
self.revCount -= count
else:
#print "minus new"
self.newCount -= count
# space other cards
self.s.statement(""" self.s.statement("""
update cards set update cards set
spaceUntil = :space, spaceUntil = :space,
@ -267,10 +280,7 @@ where id != :id and factId = :factId""",
entry = CardHistoryEntry(card, ease, lastDelay) entry = CardHistoryEntry(card, ease, lastDelay)
entry.writeSQL(self.s) entry.writeSQL(self.s)
self.modified = now self.modified = now
# update isDue for failed cards #print "ans", time.time() - t
self.markExpiredCardsDue()
# invalidate counts
self._countsDirty = True
# Queue/cache management # Queue/cache management
########################################################################## ##########################################################################
@ -287,36 +297,67 @@ then 1 -- review
else 2 -- new else 2 -- new
end)""" + where) end)""" + where)
def markExpiredCardsDue(self): def rebuildCounts(self):
"Mark expired cards due, and update their relativeDelay." t = time.time()
self.s.statement("""update cards self.cardCount = self.s.scalar("select count(*) from cards")
set isDue = 1, relativeDelay = interval / (strftime("%s", "now") - due + 1) self.factCount = self.s.scalar("select count(*) from facts")
where isDue = 0 and priority in (1,2,3,4) and combinedDue < :now""", self.failedNowCount = self.s.scalar(
now=time.time()) "select count(*) from failedCardsNow")
self.failedSoonCount = cardCount = self.s.scalar(
"select count(*) from failedCardsSoon")
self.revCount = self.s.scalar("select count(*) from revCards")
self.newCount = self.s.scalar("select count(*) from acqCardsOrdered")
print "rebuild counts", time.time() - t
def updateRelativeDelays(self): def checkDue(self):
"Update relative delays for expired cards." "Mark expired cards due, and update counts."
self.s.statement("""update cards t2 = time.time()
set relativeDelay = interval / (strftime("%s", "now") - due + 1) t = time.time()
where isDue = 1 and type = 1""") # mark due & update counts
stmt = """
update cards set
isDue = 1 where type = %d and isDue = 0 and
priority in (1,2,3,4) and combinedDue < :now"""
self.failedNowCount += self.s.statement(
stmt % 0, now=time.time()).rowcount
#print "set1", time.time() - t; t = time.time()
#self.engine.echo = True
self.revCount += self.s.statement(
stmt % 1, now=time.time()).rowcount
#self.engine.echo = False
#print "set2", time.time() - t; t = time.time()
self.newCount += self.s.statement(
stmt % 2, now=time.time()).rowcount
#print "set3", time.time() - t; t = time.time()
self.failedSoonCount = (self.failedNowCount +
self.s.scalar("""
select count(*) from cards where
type = 0 and isDue = 0 and priority in (1,2,3,4)
and combinedDue <= (select max(delay0, delay1) +
strftime("%s", "now")+1 from decks)"""))
#print "set4", time.time() - t; t = time.time()
# new card handling
self.newCountToday = max(min(
self.newCount, self.newCardsPerDay -
self.newCardsToday()), 0)
#print "me indv", time.time() - t2
def rebuildQueue(self, updateRelative=True): def rebuildQueue(self):
"Update relative delays based on current time." "Update relative delays based on current time."
if updateRelative: t = time.time()
self.updateRelativeDelays() # setup global/daily stats
self.markExpiredCardsDue()
# cache global/daily stats
self._globalStats = globalStats(self) self._globalStats = globalStats(self)
self._dailyStats = dailyStats(self) self._dailyStats = dailyStats(self)
# mark due cards and update counts
self.checkDue()
# invalid card count # invalid card count
self._countsDirty = True
# determine new card distribution # determine new card distribution
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE: if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
if self.newCountLeftToday: if self.newCountToday:
self.newCardModulus = ((self.newCountLeftToday + self.reviewCount) self.newCardModulus = (
/ self.newCountLeftToday) (self.newCountToday + self.revCount) / self.newCountToday)
# if there are cards to review, ensure modulo >= 2 # if there are cards to review, ensure modulo >= 2
if self.reviewCount: if self.revCount:
self.newCardModulus = max(2, self.newCardModulus) self.newCardModulus = max(2, self.newCardModulus)
else: else:
self.newCardModulus = 0 self.newCardModulus = 0
@ -324,6 +365,9 @@ where isDue = 1 and type = 1""")
self.averageFactor = (self.s.scalar( self.averageFactor = (self.s.scalar(
"select avg(factor) from cards where type = 1") "select avg(factor) from cards where type = 1")
or Deck.initialFactor) or Deck.initialFactor)
# recache css
self.rebuildCSS()
#print "rebuild queue", time.time() - t
def checkDailyStats(self): def checkDailyStats(self):
# check if the day has rolled over # check if the day has rolled over
@ -336,8 +380,11 @@ where isDue = 1 and type = 1""")
def nextInterval(self, card, ease): def nextInterval(self, card, ease):
"Return the next interval for CARD given EASE." "Return the next interval for CARD given EASE."
delay = self._adjustedDelay(card, ease) delay = self._adjustedDelay(card, ease)
return self._nextInterval(card.interval, card.factor, delay, ease)
def _nextInterval(self, interval, factor, delay, ease):
# if interval is less than mid interval, use presets # if interval is less than mid interval, use presets
if card.interval < self.hardIntervalMin: if interval < self.hardIntervalMin:
if ease < 2: if ease < 2:
interval = NEW_INTERVAL interval = NEW_INTERVAL
elif ease == 2: elif ease == 2:
@ -354,19 +401,19 @@ where isDue = 1 and type = 1""")
else: else:
# otherwise, multiply the old interval by a factor # otherwise, multiply the old interval by a factor
if ease == 1: if ease == 1:
factor = 1 / card.factor / 2.0 factor = 1 / factor / 2.0
interval = card.interval * factor interval = interval * factor
elif ease == 2: elif ease == 2:
factor = 1.2 factor = 1.2
interval = (card.interval + delay/4) * factor interval = (interval + delay/4) * factor
elif ease == 3: elif ease == 3:
factor = card.factor factor = factor
interval = (card.interval + delay/2) * factor interval = (interval + delay/2) * factor
elif ease == 4: elif ease == 4:
factor = card.factor * self.factorFour factor = factor * self.factorFour
interval = (card.interval + delay) * factor interval = (interval + delay) * factor
assert card.fuzz fuzz = random.uniform(0.95, 1.05)
interval *= card.fuzz interval *= fuzz
if self.maxScheduleTime: if self.maxScheduleTime:
interval = min(interval, self.maxScheduleTime) interval = min(interval, self.maxScheduleTime)
return interval return interval
@ -449,8 +496,8 @@ 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."""
return self.s.scalar(""" return self.s.scalar("""
select combinedDue from cards where priority != 0 and type != 2 select combinedDue from cards where priority in (1,2,3,4) and
order by combinedDue limit 1""") type in (0, 1) order by combinedDue limit 1""")
def earliestTimeStr(self, next=None): def earliestTimeStr(self, next=None):
"""Return the relative time to the earliest card as a string.""" """Return the relative time to the earliest card as a string."""
@ -465,7 +512,7 @@ order by combinedDue limit 1""")
"Number of cards due at TIME. Ignore new cards" "Number of cards due at TIME. Ignore new cards"
return self.s.scalar(""" return self.s.scalar("""
select count(id) from cards where combinedDue < :time select count(id) from cards where combinedDue < :time
and priority != 0 and type != 2""", time=time) and priority in (1,2,3,4) and type in (0, 1)""", time=time)
def nextIntervalStr(self, card, ease, short=False): def nextIntervalStr(self, card, ease, short=False):
"Return the next interval for CARD given EASE as a string." "Return the next interval for CARD given EASE as a string."
@ -575,25 +622,14 @@ suspended</a> cards.''') % {
# Card/fact counts - all in deck, not just due # Card/fact counts - all in deck, not just due
########################################################################## ##########################################################################
def cardCount(self):
return self.s.scalar(
"select count(id) from cards")
def factCount(self):
return self.s.scalar(
"select count(id) from facts")
def suspendedCardCount(self): def suspendedCardCount(self):
return self.s.scalar( return self.s.scalar("""
"select count(id) from cards where priority = 0") select count(id) from cards where type in (0,1,2) and isDue in (0,1)
and priority = 0""")
def seenCardCount(self): def seenCardCount(self):
return self.s.scalar( return self.s.scalar(
"select count(id) from cards where reps != 0") "select count(id) from cards where type in (0, 1)")
def newCardCount(self):
return self.s.scalar(
"select count(id) from cards where reps = 0")
# Counts related to due cards # Counts related to due cards
########################################################################## ##########################################################################
@ -605,56 +641,14 @@ suspended</a> cards.''') % {
self._dailyStats.newEase3 + self._dailyStats.newEase3 +
self._dailyStats.newEase4) self._dailyStats.newEase4)
def updateCounts(self):
"Update failed/rev/new counts if cache is dirty."
if self._countsDirty:
self._failedCount = self.s.scalar("""
select count(id) from failedCardsSoon""")
self._failedDueNowCount = self.s.scalar("""
select count(id) from failedCardsNow""")
self._reviewCount = self.s.scalar(
"select count(isDue) from cards where isDue = 1 and type = 1")
self._newCount = self.s.scalar(
"select count(isDue) from cards where isDue = 1 and type = 2")
if getattr(self, '_dailyStats', None):
self._newCountLeftToday = max(min(
self._newCount, self.newCardsPerDay -
self.newCardsToday()), 0)
self._countsDirty = False
def _getFailedCount(self):
self.updateCounts()
return self._failedCount
failedCount = property(_getFailedCount)
def _getFailedDueNowCount(self):
self.updateCounts()
return self._failedDueNowCount
failedDueNowCount = property(_getFailedDueNowCount)
def _getReviewCount(self):
self.updateCounts()
return self._reviewCount
reviewCount = property(_getReviewCount)
def _getNewCount(self):
self.updateCounts()
return self._newCount
newCount = property(_getNewCount)
def _getNewCountLeftToday(self):
self.updateCounts()
return self._newCountLeftToday
newCountLeftToday = property(_getNewCountLeftToday)
def spacedCardCount(self): def spacedCardCount(self):
return self.s.scalar(""" return self.s.scalar("""
select count(cards.id) from cards where select count(cards.id) from cards where
priority != 0 and due < :now and spaceUntil > :now""", type in (1,2) and isDue = 0 and priority in (1,2,3,4) and due < :now""",
now=time.time()) now=time.time())
def isEmpty(self): def isEmpty(self):
return self.cardCount() == 0 return not self.cardCount
def matureCardCount(self): def matureCardCount(self):
return self.s.scalar( return self.s.scalar(
@ -699,9 +693,9 @@ priority != 0 and due < :now and spaceUntil > :now""",
"Return some commonly needed stats." "Return some commonly needed stats."
stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats) stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats)
# add scheduling related stats # add scheduling related stats
stats['new'] = self.newCountLeftToday stats['new'] = self.newCountToday
stats['failed'] = self.failedCount stats['failed'] = self.failedSoonCount
stats['successive'] = self.reviewCount stats['successive'] = self.revCount
if stats['dAverageTime']: if stats['dAverageTime']:
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE: if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
count = stats['successive'] + stats['new'] count = stats['successive'] + stats['new']
@ -725,9 +719,6 @@ priority != 0 and due < :now and spaceUntil > :now""",
return "failed" return "failed"
elif card.reps: elif card.reps:
return "rev" return "rev"
else:
sys.stderr.write("couldn't determine queue for %s" %
`card.__dict__`)
# Facts # Facts
########################################################################## ##########################################################################
@ -754,16 +745,18 @@ priority != 0 and due < :now and spaceUntil > :now""",
cards = [] cards = []
self.refresh() self.refresh()
self.s.save(fact) self.s.save(fact)
self.factCount += 1
self.flushMod() self.flushMod()
for cardModel in cms: for cardModel in cms:
card = anki.cards.Card(fact, cardModel) card = anki.cards.Card(fact, cardModel)
self.flushMod() self.flushMod()
self.updatePriority(card) self.updatePriority(card)
cards.append(card) cards.append(card)
self.cardCount += len(cards)
self.newCount += len(cards)
# keep track of last used tags for convenience # keep track of last used tags for convenience
self.lastTags = fact.tags self.lastTags = fact.tags
self.setModified() self.setModified()
self._countsDirty = True
return cards return cards
def availableCardModels(self, fact): def availableCardModels(self, fact):
@ -822,9 +815,11 @@ where factId = :fid and cardModelId = :cmid""",
self.s.statement("insert into cardsDeleted select id, :time " self.s.statement("insert into cardsDeleted select id, :time "
"from cards where factId = :factId", "from cards where factId = :factId",
time=time.time(), factId=factId) time=time.time(), factId=factId)
self.s.statement("delete from cards where factId = :id", id=factId) self.cardCount -= self.s.statement(
"delete from cards where factId = :id", id=factId).rowcount
# and then the fact # and then the fact
self.s.statement("delete from facts where id = :id", id=factId) self.factCount -= self.s.statement(
"delete from facts where id = :id", id=factId).rowcount
self.s.statement("delete from fields where factId = :id", id=factId) self.s.statement("delete from fields where factId = :id", id=factId)
self.s.statement("insert into factsDeleted values (:id, :time)", self.s.statement("insert into factsDeleted values (:id, :time)",
id=factId, time=time.time()) id=factId, time=time.time())
@ -837,7 +832,8 @@ where factId = :fid and cardModelId = :cmid""",
self.s.flush() self.s.flush()
now = time.time() now = time.time()
strids = ids2str(ids) strids = ids2str(ids)
self.s.statement("delete from facts where id in %s" % strids) self.factCount -= self.s.statement(
"delete from facts where id in %s" % strids).rowcount
self.s.statement("delete from fields where factId in %s" % strids) self.s.statement("delete from fields where factId in %s" % strids)
data = [{'id': id, 'time': now} for id in ids] data = [{'id': id, 'time': now} for id in ids]
self.s.statements("insert into factsDeleted values (:id, :time)", data) self.s.statements("insert into factsDeleted values (:id, :time)", data)
@ -858,7 +854,8 @@ where facts.id not in (select factId from cards)""")
"Delete a card given its id. Delete any unused facts. Don't flush." "Delete a card given its id. Delete any unused facts. Don't flush."
self.s.flush() self.s.flush()
factId = self.s.scalar("select factId from cards where id=:id", id=id) factId = self.s.scalar("select factId from cards where id=:id", id=id)
self.s.statement("delete from cards where id = :id", id=id) self.cardCount -= self.s.statement(
"delete from cards where id = :id", id=id).rowcount
self.s.statement("insert into cardsDeleted values (:id, :time)", self.s.statement("insert into cardsDeleted values (:id, :time)",
id=id, time=time.time()) id=id, time=time.time())
if factId and not self.factUseCount(factId): if factId and not self.factUseCount(factId):
@ -876,7 +873,8 @@ where facts.id not in (select factId from cards)""")
factIds = self.s.column0("select factId from cards where id in %s" factIds = self.s.column0("select factId from cards where id in %s"
% strids) % strids)
# drop from cards # drop from cards
self.s.statement("delete from cards where id in %s" % strids) self.cardCount -= self.s.statement(
"delete from cards where id in %s" % strids).rowcount
# note deleted # note deleted
data = [{'id': id, 'time': now} for id in ids] data = [{'id': id, 'time': now} for id in ids]
self.s.statements("insert into cardsDeleted values (:id, :time)", data) self.s.statements("insert into cardsDeleted values (:id, :time)", data)
@ -1000,6 +998,33 @@ fieldModelId = :new where fieldModelId = :old""",
self.deleteModel(model) self.deleteModel(model)
self.refresh() self.refresh()
def rebuildCSS(self):
# css for all fields
def _genCSS(prefix, row):
(id, fam, siz, col, align) = row
t = ""
if fam: t += 'font-family:"%s";' % toPlatformFont(fam)
if siz: t += 'font-size:%dpx;' % siz
if col: t += 'color:%s;' % col
if align != -1:
if align == 0: align = "center"
elif align == 1: align = "left"
else: align = "right"
t += 'text-align:%s;' % align
if t:
t = "%s%s {%s}\n" % (prefix, hexifyID(id), t)
return t
css = "".join([_genCSS(".fm", row) for row in self.s.all("""
select id, quizFontFamily, quizFontSize, quizFontColour, -1 from fieldModels""")])
css += "".join([_genCSS("#cmq", row) for row in self.s.all("""
select id, questionFontFamily, questionFontSize, questionFontColour,
questionAlign from cardModels""")])
css += "".join([_genCSS("#cma", row) for row in self.s.all("""
select id, answerFontFamily, answerFontSize, answerFontColour,
answerAlign from cardModels""")])
self.css = css
return css
# Fields # Fields
########################################################################## ##########################################################################
@ -1089,35 +1114,45 @@ cardModelId = :id""", id=cardModel.id)
model.setModified() model.setModified()
self.flushMod() self.flushMod()
def updateCardsFromModel(self, cardModel): def updateCardsFromModel(self, model):
"Update all card question/answer when model changes." "Update all card question/answer when model changes."
ids = self.s.all(""" ids = self.s.all("""
select cards.id, cards.cardModelId, cards.factId from select cards.id, cards.cardModelId, cards.factId, facts.modelId from
cards, facts, cardmodels where cards, facts where
cards.factId = facts.id and cards.factId = facts.id and
facts.modelId = cardModels.modelId and facts.modelId = :id""", id=model.id)
cards.cardModelId = :id""", id=cardModel.id)
if not ids: if not ids:
return return
self.updateCardQACache(ids) self.updateCardQACache(ids)
def updateCardQACache(self, ids, dirty=True): def updateCardQACache(self, ids, dirty=True):
"Given a list of (cardId, cardModelId, factId), update q/a cache." "Given a list of (cardId, cardModelId, factId, modId), update q/a cache."
if dirty: if dirty:
mod = ", modified = %f" % time.time() mod = ", modified = %f" % time.time()
else: else:
mod = "" mod = ""
cms = self.s.query(CardModel).all() # tags
for cm in cms: tags = dict(self.tagsList(priority="", where="and cards.id in %s" %
pend = [{'q': cm.renderQASQL('q', fid), ids2str([x[0] for x in ids])))
'a': cm.renderQASQL('a', fid), facts = {}
'id': cid} # fields
for (cid, cmid, fid) in ids if cmid == cm.id] for k, g in groupby(self.s.all("""
if pend: select fields.factId, fieldModels.name, fieldModels.id, fields.value
self.s.execute(""" from fields, fieldModels where fields.factId in %s and
fields.fieldModelId = fieldModels.id
order by fields.factId""" % ids2str([x[2] for x in ids])),
itemgetter(0)):
facts[k] = dict([(r[1], (r[2], r[3])) for r in g])
# card models
cms = {}
for c in self.s.query(CardModel).all():
cms[c.id] = c
pend = [formatQA(cid, mid, facts[fid], tags[cid], cms[cmid])
for (cid, cmid, fid, mid) in ids]
if pend:
self.s.execute("""
update cards set update cards set
question = :q, question = :question, answer = :answer
answer = :a
%s %s
where id = :id""" % mod, pend) where id = :id""" % mod, pend)
@ -1135,13 +1170,13 @@ where cardModelId in %s""" % strids, now=time.time())
# Tags # Tags
########################################################################## ##########################################################################
def tagsList(self, where=""): def tagsList(self, where="", priority=", cards.priority"):
"Return a list of (cardId, allTags, priority)" "Return a list of (cardId, allTags, priority)"
return self.s.all(""" return self.s.all("""
select cards.id, cards.tags || "," || facts.tags || "," || models.tags || "," || select cards.id, cards.tags || "," || facts.tags || "," || models.tags || "," ||
cardModels.name, cards.priority from cards, facts, models, cardModels where cardModels.name %s from cards, facts, models, cardModels where
cards.factId == facts.id and facts.modelId == models.id cards.factId == facts.id and facts.modelId == models.id
and cards.cardModelId = cardModels.id %s""" % where) and cards.cardModelId = cardModels.id %s""" % (priority, where))
def allTags(self): def allTags(self):
"Return a hash listing tags in model, fact and cards." "Return a hash listing tags in model, fact and cards."
@ -1419,10 +1454,8 @@ select id from fields where factId not in (select id from facts)""")
"update fields set value=:value where id=:id", "update fields set value=:value where id=:id",
newFields) newFields)
# regenerate question/answer cache # regenerate question/answer cache
cms = self.s.query(CardModel).all() for m in self.models:
for cm in cms: self.updateCardsFromModel(m)
self.updateCardsFromModel(cm)
self.s.expunge(cm)
# forget all deletions # forget all deletions
self.s.statement("delete from cardsDeleted") self.s.statement("delete from cardsDeleted")
self.s.statement("delete from factsDeleted") self.s.statement("delete from factsDeleted")
@ -1434,6 +1467,8 @@ select id from fields where factId not in (select id from facts)""")
self.s.statement("update facts set modified = :t", t=time.time()) self.s.statement("update facts set modified = :t", t=time.time())
self.s.statement("update models set modified = :t", t=time.time()) self.s.statement("update models set modified = :t", t=time.time())
self.lastSync = 0 self.lastSync = 0
# update counts
self.rebuildCounts()
# update deck and save # update deck and save
self.flushMod() self.flushMod()
self.save() self.save()
@ -1537,6 +1572,19 @@ alter table decks add column sessionTimeLimit integer not null default 1800""")
if ver < 11: if ver < 11:
s.execute(""" s.execute("""
alter table decks add column utcOffset numeric(10, 2) not null default 0""") alter table decks add column utcOffset numeric(10, 2) not null default 0""")
if ver < 12:
s.execute("""
alter table decks add column cardCount integer not null default 0""")
s.execute("""
alter table decks add column factCount integer not null default 0""")
s.execute("""
alter table decks add column failedNowCount integer not null default 0""")
s.execute("""
alter table decks add column failedSoonCount integer not null default 0""")
s.execute("""
alter table decks add column revCount integer not null default 0""")
s.execute("""
alter table decks add column newCount integer not null default 0""")
deck = s.query(Deck).get(1) deck = s.query(Deck).get(1)
# attach db vars # attach db vars
deck.path = path deck.path = path
@ -1607,23 +1655,20 @@ alter table decks add column utcOffset numeric(10, 2) not null default 0""")
"Add indices to the DB." "Add indices to the DB."
# card queues # card queues
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_cards_markExpired on cards create index if not exists ix_cards_checkDueOrder on cards
(isDue, priority desc, combinedDue desc)""") (type, isDue, priority desc, combinedDue desc)""")
deck.s.statement("""
create index if not exists ix_cards_failedIsDue on cards
(type, isDue, combinedDue)""")
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_cards_failedOrder on cards create index if not exists ix_cards_failedOrder on cards
(type, isDue, due)""") (type, isDue, priority desc, due)""")
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_cards_revisionOrder on cards create index if not exists ix_cards_revisionOrder on cards
(type, isDue, priority desc, relativeDelay)""") (type, isDue, priority desc, interval desc)""")
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_cards_newRandomOrder on cards create index if not exists ix_cards_newRandomOrder on cards
(priority desc, factId, ordinal)""") (type, isDue, priority desc, factId, ordinal)""")
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_cards_newOrderedOrder on cards create index if not exists ix_cards_newOrderedOrder on cards
(priority desc, due)""") (type, isDue, priority desc, due)""")
# card spacing # card spacing
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_cards_factId on cards (factId)""") create index if not exists ix_cards_factId on cards (factId)""")
@ -1661,36 +1706,34 @@ create index if not exists ix_mediaDeleted_factId on mediaDeleted (mediaId)""")
s.statement("drop view if exists typedCards") s.statement("drop view if exists typedCards")
s.statement("drop view if exists failedCards") s.statement("drop view if exists failedCards")
s.statement("drop view if exists failedCardsNow") s.statement("drop view if exists failedCardsNow")
s.statement("drop view if exists failedCardsSoon")
s.statement("drop view if exists revCards")
s.statement("drop view if exists acqCardsRandom")
s.statement("drop view if exists acqCardsOrdered")
s.statement(""" s.statement("""
create view failedCardsNow as create view failedCardsNow as
select * from cards select * from cards
where type = 0 and isDue = 1 where type = 0 and isDue = 1
and combinedDue <= (strftime("%s", "now") + 1) order by due
order by combinedDue
""") """)
s.statement("drop view if exists failedCardsSoon")
s.statement(""" s.statement("""
create view failedCardsSoon as create view failedCardsSoon as
select * from cards select * from cards
where type = 0 and priority != 0 where type = 0 and priority in (1,2,3,4)
and combinedDue <= and combinedDue <= (select max(delay0, delay1) +
(select max(delay0, delay1)+strftime("%s", "now")+1 strftime("%s", "now")+1 from decks)
from decks)
order by modified order by modified
""") """)
s.statement("drop view if exists revCards")
s.statement(""" s.statement("""
create view revCards as create view revCards as
select * from cards where select * from cards
type = 1 and isDue = 1 where type = 1 and isDue = 1
order by type, isDue, priority desc, relativeDelay""") order by priority desc, interval desc""")
s.statement("drop view if exists acqCardsRandom")
s.statement(""" s.statement("""
create view acqCardsRandom as create view acqCardsRandom as
select * from cards select * from cards
where type = 2 and isDue = 1 where type = 2 and isDue = 1
order by priority desc, factId, ordinal""") order by priority desc, factId, ordinal""")
s.statement("drop view if exists acqCardsOrdered")
s.statement(""" s.statement("""
create view acqCardsOrdered as create view acqCardsOrdered as
select * from cards select * from cards
@ -1838,6 +1881,24 @@ alter table models add column source integer not null default 0""")
DeckStorage._setUTCOffset(deck) DeckStorage._setUTCOffset(deck)
deck.version = 11 deck.version = 11
deck.s.commit() deck.s.commit()
if deck.version < 12: #True: # False: #True: #deck.version < 12:
deck.s.statement("drop index if exists ix_cards_revisionOrder")
deck.s.statement("drop index if exists ix_cards_newRandomOrder")
deck.s.statement("drop index if exists ix_cards_newOrderedOrder")
deck.s.statement("drop index if exists ix_cards_markExpired")
deck.s.statement("drop index if exists ix_cards_failedIsDue")
deck.s.statement("drop index if exists ix_cards_failedOrder")
deck.s.statement("drop index if exists ix_cards_type")
deck.s.statement("drop index if exists ix_cards_priority")
DeckStorage._addViews(deck)
DeckStorage._addIndices(deck)
deck.rebuildCounts()
deck.rebuildQueue()
# regenerate question/answer cache
for m in deck.models:
deck.updateCardsFromModel(m)
deck.version = 12
deck.s.commit()
return deck return deck
_upgradeDeck = staticmethod(_upgradeDeck) _upgradeDeck = staticmethod(_upgradeDeck)

View file

@ -113,6 +113,7 @@ modified = :now
bulkClient.server = bulkServer bulkClient.server = bulkServer
bulkClient.sync() bulkClient.sync()
# need to save manually # need to save manually
self.newDeck.rebuildCounts()
self.newDeck.s.commit() self.newDeck.s.commit()
self.newDeck.close() self.newDeck.close()

View file

@ -11,7 +11,7 @@ __docformat__ = 'restructuredtext'
import time import time
from anki.db import * from anki.db import *
from anki.errors import * from anki.errors import *
from anki.models import Model, FieldModel, fieldModelsTable from anki.models import Model, FieldModel, fieldModelsTable, formatQA
from anki.utils import genID from anki.utils import genID
from anki.features import FeatureManager from anki.features import FeatureManager
@ -95,9 +95,6 @@ class Fact(object):
except IndexError: except IndexError:
return default return default
def css(self):
return "".join([f.fieldModel.css() for f in self.fields])
def assertValid(self): def assertValid(self):
"Raise an error if required fields are empty." "Raise an error if required fields are empty."
for field in self.fields: for field in self.fields:
@ -135,9 +132,13 @@ class Fact(object):
"Mark modified and update cards." "Mark modified and update cards."
self.modified = time.time() self.modified = time.time()
if textChanged: if textChanged:
d = {}
for f in self.model.fieldModels:
d[f.name] = (f.id, self[f.name])
for card in self.cards: for card in self.cards:
card.question = card.cardModel.renderQA(card, self, "question") qa = formatQA(None, self.modelId, d, card.allTags(), card.cardModel)
card.answer = card.cardModel.renderQA(card, self, "answer") card.question = qa['question']
card.answer = qa['answer']
card.setModified() card.setModified()
# Fact deletions # Fact deletions

View file

@ -123,6 +123,7 @@ all but one card model."""))
[{'modelId': self.model.id, [{'modelId': self.model.id,
'tags': self.tagsToAdd, 'tags': self.tagsToAdd,
'id': factIds[n]} for n in range(len(cards))]) 'id': factIds[n]} for n in range(len(cards))])
self.deck.factCount += len(factIds)
self.deck.s.execute(""" self.deck.s.execute("""
delete from factsDeleted delete from factsDeleted
where factId in (%s)""" % ",".join([str(s) for s in factIds])) where factId in (%s)""" % ",".join([str(s) for s in factIds]))
@ -151,11 +152,13 @@ where factId in (%s)""" % ",".join([str(s) for s in factIds]))
'factId': factIds[m], 'factId': factIds[m],
'cardModelId': cm.id, 'cardModelId': cm.id,
'ordinal': cm.ordinal, 'ordinal': cm.ordinal,
'question': cm.renderQASQL('q', factIds[m]), 'question': u"",
'answer': cm.renderQASQL('a', factIds[m]), 'answer': u"",
'type': 2},cards[m]) for m in range(len(cards))] 'type': 2},cards[m]) for m in range(len(cards))]
self.deck.s.execute(cardsTable.insert(), self.deck.s.execute(cardsTable.insert(),
data) data)
self.deck.updateCardsFromModel(self.model)
self.deck.cardCount += len(cards)
self.total = len(factIds) self.total = len(factIds)
def addMeta(self, data, card): def addMeta(self, data, card):

View file

@ -1,285 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
Importing Anki v0.3 decks
==========================
"""
__docformat__ = 'restructuredtext'
import cPickle, cStringIO, os, datetime, types, sys
from anki.models import Model, FieldModel, CardModel
from anki.facts import Fact, factsTable, fieldsTable
from anki.cards import cardsTable
from anki.stats import *
from anki.features import FeatureManager
from anki.importing import Importer
from anki.utils import genID
def transformClasses(m, c):
"Map objects into dummy classes"
class EmptyClass(object):
pass
class EmptyList(list):
pass
class EmptyDict(dict):
pass
if c == "Fact":
return EmptyDict
elif c in ("CardModels", "Deck", "Facts", "Fields", "Models"):
return EmptyList
return EmptyClass
def load(path):
"Load a deck from PATH."
if isinstance(path, types.UnicodeType):
path = path.encode(sys.getfilesystemencoding())
file = open(path, "rb")
try:
try:
data = file.read()
except (IOError, OSError):
raise ImportError
finally:
file.close()
unpickler = cPickle.Unpickler(cStringIO.StringIO(data))
unpickler.find_global = transformClasses
deck = unpickler.load()
deck.path = unicode(os.path.abspath(path), sys.getfilesystemencoding())
return deck
# we need to upgrade the deck before converting
def maybeUpgrade(deck):
# change old hardInterval from 1 day to 8-12 hours
if list(deck.sched.hardInterval) == [1.0, 1.0]:
deck.sched.hardInterval = [0.3333, 0.5]
# add 'medium priority' support
if not hasattr(deck.sched, 'medPriority'):
deck.sched.medPriority = []
# add delay2
if not hasattr(deck.sched, "delay2"):
deck.sched.delay2 = 28800
# add collapsing
if not hasattr(deck.sched, "collapse"):
deck.sched.collapse = 18000
# card related
# - add 'total' attribute
for card in deck:
card.__dict__['total'] = (
card.stats['new']['yes'] +
card.stats['new']['no'] +
card.stats['young']['yes'] +
card.stats['young']['no'] +
card.stats['old']['yes'] +
card.stats['old']['no'])
class Anki03Importer(Importer):
needMapper = False
def doImport(self):
oldDeck = load(self.file)
maybeUpgrade(oldDeck)
# mappings for old->new ids
cardModels = {}
fieldModels = {}
# start with the models
s = self.deck.s
deck = self.deck
import types
def uni(txt):
if txt is None:
return txt
if not isinstance(txt, types.UnicodeType):
txt = unicode(txt, "utf-8")
return txt
for oldModel in oldDeck.facts.models:
model = Model(uni(oldModel.name), uni(oldModel.description))
model.id = oldModel.id
model.tags = u", ".join(oldModel.tags)
model.features = u", ".join(oldModel.decorators)
model.created = oldModel.created
model.modified = oldModel.modified
deck.newCardOrder = min(1, oldModel.position)
deck.addModel(model)
# fields
for oldField in oldModel.fields:
fieldModel = FieldModel(uni(oldField.name),
uni(oldField.description),
oldField.name in oldModel.required,
oldField.name in oldModel.unique)
fieldModel.features = u", ".join(oldField.features)
fieldModel.quizFontFamily = uni(oldField.display['quiz']['fontFamily'])
fieldModel.quizFontSize = oldField.display['quiz']['fontSize']
fieldModel.quizFontColour = uni(oldField.display['quiz']['fontColour'])
fieldModel.editFontFamily = uni(oldField.display['edit']['fontFamily'])
fieldModel.editFontSize = oldField.display['edit']['fontSize']
fieldModel.id = oldField.id
model.addFieldModel(fieldModel)
s.flush() # we need the id
fieldModels[id(oldField)] = fieldModel
# card models
for oldCard in oldModel.allcards:
cardModel = CardModel(uni(oldCard.name),
uni(oldCard.description),
uni(oldCard.qformat),
uni(oldCard.aformat))
cardModel.active = oldCard in oldModel.cards
cardModel.questionInAnswer = oldCard.questionInAnswer
cardModel.id = oldCard.id
model.spacing = 0.25
cardModel.questionFontFamily = uni(oldCard.display['question']['fontFamily'])
cardModel.questionFontSize = oldCard.display['question']['fontSize']
cardModel.questionFontColour = uni(oldCard.display['question']['fontColour'])
cardModel.questionAlign = oldCard.display['question']['align']
cardModel.answerFontFamily = uni(oldCard.display['answer']['fontFamily'])
cardModel.answerFontSize = oldCard.display['answer']['fontSize']
cardModel.answerFontColour = uni(oldCard.display['answer']['fontColour'])
cardModel.answerAlign = oldCard.display['answer']['align']
cardModel.lastFontFamily = uni(oldCard.display['last']['fontFamily'])
cardModel.lastFontSize = oldCard.display['last']['fontSize']
cardModel.lastFontColour = uni(oldCard.display['last']['fontColour'])
cardModel.editQuestionFontFamily = (
uni(oldCard.display['editQuestion']['fontFamily']))
cardModel.editQuestionFontSize = (
oldCard.display['editQuestion']['fontSize'])
cardModel.editAnswerFontFamily = (
uni(oldCard.display['editAnswer']['fontFamily']))
cardModel.editAnswerFontSize = (
oldCard.display['editAnswer']['fontSize'])
model.addCardModel(cardModel)
s.flush() # we need the id
cardModels[id(oldCard)] = cardModel
# facts
def getSpace(lastCard, lastAnswered):
if not lastCard:
return 0
return lastAnswered + lastCard.delay
def getLastCardId(fact):
if not fact.lastCard:
return None
ret = [c.id for c in fact.cards if c.model.id == fact.lastCard.id]
if ret:
return ret[0]
d = [{'id': f.id,
'modelId': f.model.id,
'created': f.created,
'modified': f.modified,
'tags': u",".join(f.tags),
'spaceUntil': getSpace(f.lastCard, f.lastAnswered),
'lastCardId': getLastCardId(f)
} for f in oldDeck.facts]
if d:
s.execute(factsTable.insert(), d)
self.total = len(oldDeck.facts)
# fields in facts
toAdd = []
for oldFact in oldDeck.facts:
for field in oldFact.model.fields:
toAdd.append({'factId': oldFact.id,
'id': genID(),
'fieldModelId': fieldModels[id(field)].id,
'ordinal': fieldModels[id(field)].ordinal,
'value': uni(oldFact.get(field.name, u""))})
if toAdd:
s.execute(fieldsTable.insert(), toAdd)
# cards
class FakeObj(object):
pass
fake = FakeObj()
fake.fact = FakeObj()
fake.fact.model = FakeObj()
fake.cardModel = FakeObj()
def renderQA(c, type):
fake.tags = u", ".join(c.tags)
fake.fact.tags = u", ".join(c.fact.tags)
fake.fact.model.tags = u", ".join(c.fact.model.tags)
fake.cardModel.name = c.model.name
return cardModels[id(c.model)].renderQA(fake, c.fact, type)
d = [{'id': c.id,
'created': c.created,
'modified': c.modified,
'factId': c.fact.id,
'ordinal': cardModels[id(c.model)].ordinal,
'cardModelId': cardModels[id(c.model)].id,
'tags': u", ".join(c.tags),
'factor': 2.5,
'firstAnswered': c.firstAnswered,
'interval': c.interval,
'lastInterval': c.lastInterval,
'modified': c.modified,
'due': c.nextTime,
'lastDue': c.lastTime,
'reps': c.total,
'question': renderQA(c, 'question'),
'answer': renderQA(c, 'answer'),
'averageTime': c.stats['averageTime'],
'reviewTime': c.stats['totalTime'],
'yesCount': (c.stats['new']['yes'] +
c.stats['young']['yes'] +
c.stats['old']['yes']),
'noCount': (c.stats['new']['no'] +
c.stats['young']['no'] +
c.stats['old']['no']),
'successive': c.stats['successivelyCorrect']}
for c in oldDeck]
if d:
s.execute(cardsTable.insert(), d)
# scheduler
deck.description = uni(oldDeck.description)
deck.created = oldDeck.created
deck.maxScheduleTime = oldDeck.sched.maxScheduleTime
deck.hardIntervalMin = oldDeck.sched.hardInterval[0]
deck.hardIntervalMax = oldDeck.sched.hardInterval[1]
deck.midIntervalMin = oldDeck.sched.midInterval[0]
deck.midIntervalMax = oldDeck.sched.midInterval[1]
deck.easyIntervalMin = oldDeck.sched.easyInterval[0]
deck.easyIntervalMax = oldDeck.sched.easyInterval[1]
deck.delay0 = oldDeck.sched.delay0
deck.delay1 = oldDeck.sched.delay1
deck.delay2 = oldDeck.sched.delay2
deck.collapseTime = 3600 # oldDeck.sched.collapse
deck.highPriority = u", ".join(oldDeck.sched.highPriority)
deck.medPriority = u", ".join(oldDeck.sched.medPriority)
deck.lowPriority = u", ".join(oldDeck.sched.lowPriority)
deck.suspended = u", ".join(oldDeck.sched.suspendedTags)
# scheduler global stats
stats = Stats()
stats.create(deck.s, 0, datetime.date.today())
stats.day = datetime.date.fromtimestamp(oldDeck.created)
stats.averageTime = oldDeck.sched.globalStats['averageTime']
stats.reviewTime = oldDeck.sched.globalStats['totalTime']
stats.distractedTime = 0
stats.distractedReps = 0
stats.newEase0 = oldDeck.sched.easeStats.get('new', {}).get(0, 0)
stats.newEase1 = oldDeck.sched.easeStats.get('new', {}).get(1, 0)
stats.newEase2 = oldDeck.sched.easeStats.get('new', {}).get(2, 0)
stats.newEase3 = oldDeck.sched.easeStats.get('new', {}).get(3, 0)
stats.newEase4 = oldDeck.sched.easeStats.get('new', {}).get(4, 0)
stats.youngEase0 = oldDeck.sched.easeStats.get('young', {}).get(0, 0)
stats.youngEase1 = oldDeck.sched.easeStats.get('young', {}).get(1, 0)
stats.youngEase2 = oldDeck.sched.easeStats.get('young', {}).get(2, 0)
stats.youngEase3 = oldDeck.sched.easeStats.get('young', {}).get(3, 0)
stats.youngEase4 = oldDeck.sched.easeStats.get('young', {}).get(4, 0)
stats.matureEase0 = oldDeck.sched.easeStats.get('old', {}).get(0, 0)
stats.matureEase1 = oldDeck.sched.easeStats.get('old', {}).get(1, 0)
stats.matureEase2 = oldDeck.sched.easeStats.get('old', {}).get(2, 0)
stats.matureEase3 = oldDeck.sched.easeStats.get('old', {}).get(3, 0)
stats.matureEase4 = oldDeck.sched.easeStats.get('old', {}).get(4, 0)
yesCount = (oldDeck.sched.globalStats['new']['yes'] +
oldDeck.sched.globalStats['young']['yes'] +
oldDeck.sched.globalStats['old']['yes'])
noCount = (oldDeck.sched.globalStats['new']['no'] +
oldDeck.sched.globalStats['young']['no'] +
oldDeck.sched.globalStats['old']['no'])
stats.reps = yesCount + noCount
stats.toDB(deck.s)
# ignore daily stats & history, they make no sense on new version
s.flush()
deck.updateAllPriorities()
# save without updating mod time
deck.modified = oldDeck.modified
deck.lastLoaded = deck.modified
deck.s.commit()
deck.save()

View file

@ -8,17 +8,10 @@ Media support
""" """
__docformat__ = 'restructuredtext' __docformat__ = 'restructuredtext'
try:
import hashlib
md5 = hashlib.md5
except ImportError:
import md5
md5 = md5.new
import os, stat, time, shutil, re import os, stat, time, shutil, re
from anki.db import * from anki.db import *
from anki.facts import Fact from anki.facts import Fact
from anki.utils import addTags, genID, ids2str from anki.utils import addTags, genID, ids2str, checksum
from anki.lang import _ from anki.lang import _
regexps = (("(\[sound:([^]]+)\])", regexps = (("(\[sound:([^]]+)\])",
@ -52,9 +45,6 @@ mediaDeletedTable = Table(
# Helper functions # Helper functions
########################################################################## ##########################################################################
def checksum(data):
return md5(data).hexdigest()
def mediaFilename(path): def mediaFilename(path):
"Return checksum.ext for path" "Return checksum.ext for path"
new = checksum(open(path, "rb").read()) new = checksum(open(path, "rb").read())

View file

@ -15,10 +15,11 @@ Model - define the way in which facts are added and shown
import time import time
from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.ext.orderinglist import ordering_list
from anki.db import * from anki.db import *
from anki.utils import genID from anki.utils import genID, canonifyTags, safeClassName
from anki.fonts import toPlatformFont from anki.fonts import toPlatformFont
from anki.utils import parseTags from anki.utils import parseTags, hexifyID, checksum
from anki.lang import _ from anki.lang import _
from copy import copy
def alignmentLabels(): def alignmentLabels():
return { return {
@ -58,18 +59,6 @@ class FieldModel(object):
self.unique = unique self.unique = unique
self.id = genID() self.id = genID()
def css(self, type="quiz"):
t = ".%s { " % self.name.replace(" ", "")
if getattr(self, type+'FontFamily'):
t += "font-family: \"%s\"; " % toPlatformFont(
getattr(self, type+'FontFamily'))
if getattr(self, type+'FontSize'):
t += "font-size: %dpx; " % getattr(self, type+'FontSize')
if type == "quiz" and getattr(self, type+'FontColour'):
t += "color: %s; " % getattr(self, type+'FontColour')
t += " }\n"
return t
mapper(FieldModel, fieldModelsTable) mapper(FieldModel, fieldModelsTable)
# Card models # Card models
@ -119,81 +108,28 @@ class CardModel(object):
self.active = active self.active = active
self.id = genID() self.id = genID()
def renderQA(self, card, fact, type, format="text"):
"Render fact into card based on card model."
if type == "question": field = self.qformat
elif type == "answer": field = self.aformat
htmlFields = {}
htmlFields.update(fact)
alltags = parseTags(card.tags + "," +
card.fact.tags + "," +
card.cardModel.name + "," +
card.fact.model.tags)
htmlFields['tags'] = ", ".join(alltags)
textFields = {}
textFields.update(htmlFields)
# add per-field formatting
for (k, v) in htmlFields.items():
# generate pure text entries
htmlFields["text:"+k] = v
textFields["text:"+k] = v
if v:
# convert newlines to html & add spans to fields
v = v.replace("\n", "<br>")
htmlFields[k] = '<span class="%s">%s</span>' % (k.replace(" ",""), v)
try:
html = field % htmlFields
text = field % textFields
except (KeyError, TypeError, ValueError):
return _("[invalid format; see model properties]")
if not html:
html = _("[empty]")
text = _("[empty]")
if format == "text":
return text
# add outer div & alignment (with tables due to qt's html handling)
html = '<div class="%s">%s</div>' % (type, html)
attr = type + 'Align'
if getattr(self, attr) == 0:
align = "center"
elif getattr(self, attr) == 1:
align = "left"
else:
align = "right"
html = (("<center><table width=95%%><tr><td align=%s>" % align) +
html + "</td></tr></table></center>")
return html
def renderQASQL(self, type, factId):
"Render QA in pure SQL, with no HTML generation."
fields = dict(object_session(self).all("""
select fieldModels.name, fields.value from fields, fieldModels
where
fields.factId = :factId and
fields.fieldModelId = fieldModels.id""", factId=factId))
fields['tags'] = u""
for (k, v) in fields.items():
fields["text:"+k] = v
if type == "q": format = self.qformat
else: format = self.aformat
try:
return format % fields
except (KeyError, TypeError, ValueError):
return _("[empty]")
def css(self):
"Return the CSS markup for this card."
t = ""
for type in ("question", "answer"):
t += ".%s { font-family: \"%s\"; color: %s; font-size: %dpx; }\n" % (
type,
toPlatformFont(getattr(self, type+"FontFamily")),
getattr(self, type+"FontColour"),
getattr(self, type+"FontSize"))
return t
mapper(CardModel, cardModelsTable) mapper(CardModel, cardModelsTable)
def formatQA(cid, mid, fact, tags, cm):
"Return a dict of {id, question, answer}"
d = {'id': cid}
fields = {}
for (k, v) in fact.items():
fields["text:"+k] = v[1]
fields[k] = '<span class="fm%s">%s</span>' % (
hexifyID(v[0]), v[1])
fields['tags'] = canonifyTags(tags)
# render q & a
ret = []
for (type, format) in (("question", cm.qformat),
("answer", cm.aformat)):
try:
html = format % fields
except (KeyError, TypeError, ValueError):
html = _("[invalid question/answer format]")
d[type] = html
return d
# Model table # Model table
########################################################################## ##########################################################################

View file

@ -323,8 +323,8 @@ class DeckStats(object):
d = self.deck d = self.deck
html="<h1>" + _("Deck Statistics") + "</h1>" html="<h1>" + _("Deck Statistics") + "</h1>"
html += _("Deck created: <b>%s</b> ago<br>") % self.createdTimeStr() html += _("Deck created: <b>%s</b> ago<br>") % self.createdTimeStr()
total = d.cardCount() total = d.cardCount
new = d.newCardCount() new = d.newCount
young = d.youngCardCount() young = d.youngCardCount()
old = d.matureCardCount() old = d.matureCardCount()
newP = new / float(total) * 100 newP = new / float(total) * 100
@ -352,7 +352,7 @@ class DeckStats(object):
html += _("First-seen cards: <b>%(gNewYes%)0.1f%%</b> " html += _("First-seen cards: <b>%(gNewYes%)0.1f%%</b> "
"(<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>") % stats "(<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>") % stats
# average pending time # average pending time
existing = d.cardCount() - d.newCardCount() existing = d.cardCount - d.newTodayCount
avgInt = self.getAverageInterval() avgInt = self.getAverageInterval()
def tr(a, b): def tr(a, b):
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b) return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
@ -418,7 +418,7 @@ class DeckStats(object):
def newAverage(self): def newAverage(self):
"Average number of new cards added each day." "Average number of new cards added each day."
return self.deck.cardCount() / max(1, self.ageInDays()) return self.deck.cardCount / max(1, self.ageInDays())
def createdTimeStr(self): def createdTimeStr(self):
return anki.utils.fmtTimeSpan(time.time() - self.deck.created) return anki.utils.fmtTimeSpan(time.time() - self.deck.created)

View file

@ -425,8 +425,13 @@ where factId in %s""" % factIds))
'spaceUntil': f[5], 'spaceUntil': f[5],
'lastCardId': f[6] 'lastCardId': f[6]
} for f in facts] } for f in facts]
t = time.time()
self.deck.factCount += (len(facts) - self.deck.s.scalar(
"select count(*) from facts where id in %s" %
ids2str([f[0] for f in facts])))
#print "sync check", time.time() - t
self.deck.s.execute(""" self.deck.s.execute("""
insert or replace into facts insert or ignore into facts
(id, modelId, created, modified, tags, spaceUntil, lastCardId) (id, modelId, created, modified, tags, spaceUntil, lastCardId)
values values
(:id, :modelId, :created, :modified, :tags, :spaceUntil, :lastCardId)""", dlist) (:id, :modelId, :created, :modified, :tags, :spaceUntil, :lastCardId)""", dlist)
@ -509,6 +514,11 @@ from cards where id in %s""" % ids2str(ids)))
'type': c[35], 'type': c[35],
'combinedDue': c[36], 'combinedDue': c[36],
} for c in cards] } for c in cards]
t = time.time()
self.deck.cardCount += (len(cards) - self.deck.s.scalar(
"select count(*) from cards where id in %s" %
ids2str([c[0] for c in cards])))
#print "sync check cards", time.time() - t
self.deck.s.execute(""" self.deck.s.execute("""
insert or replace into cards insert or replace into cards
(id, factId, cardModelId, created, modified, tags, ordinal, (id, factId, cardModelId, created, modified, tags, ordinal,
@ -782,6 +792,9 @@ where media.id in %s""" % sids, now=time.time())
t = time.time() t = time.time()
dlist = [{'id': c[0], 'factId': c[1], 'cardModelId': c[2], dlist = [{'id': c[0], 'factId': c[1], 'cardModelId': c[2],
'ordinal': c[3], 'created': c[4], 't': t} for c in cards] 'ordinal': c[3], 'created': c[4], 't': t} for c in cards]
self.deck.cardCount += (len(cards) - self.deck.s.scalar(
"select count(*) from cards where id in %s" %
ids2str([c[0] for c in cards])))
# add any missing cards # add any missing cards
self.deck.s.statements(""" self.deck.s.statements("""
insert or ignore into cards insert or ignore into cards
@ -800,7 +813,15 @@ values
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, "", "", 2.5, 0, 1, 2, :t, 0)""", dlist) 0, "", "", 2.5, 0, 1, 2, :t, 0)""", dlist)
# update q/as # update q/as
self.deck.updateCardQACache([(c[0], c[2], c[1]) for c in cards]) models = dict(self.deck.s.all("""
select cards.id, models.id
from cards, facts, models
where cards.factId = facts.id
and facts.modelId = models.id
and cards.id in %s""" % ids2str([c[0] for c in cards])))
self.deck.s.flush()
self.deck.updateCardQACache(
[(c[0], c[2], c[1], models[c[0]]) for c in cards])
# Tools # Tools
########################################################################## ##########################################################################
@ -972,7 +993,7 @@ class HttpSyncServer(SyncServer):
return self.stuff(SyncServer.genOneWayPayload(self, return self.stuff(SyncServer.genOneWayPayload(self,
self.unstuff(payload))) self.unstuff(payload)))
def getDecks(self, libanki, client, sources): def getDecks(self, libanki, client, sources, pversion):
return self.stuff({ return self.stuff({
"status": "OK", "status": "OK",
"decks": self.decks, "decks": self.decks,

View file

@ -10,6 +10,13 @@ __docformat__ = 'restructuredtext'
import re, os, random, time import re, os, random, time
try:
import hashlib
md5 = hashlib.md5
except ImportError:
import md5
md5 = md5.new
from anki.db import * from anki.db import *
from anki.lang import _, ngettext from anki.lang import _, ngettext
@ -204,3 +211,9 @@ The caller is responsible for ensuring only integers are provided.
This is safe if you use sqlite primary key columns, which are guaranteed This is safe if you use sqlite primary key columns, which are guaranteed
to be integers.""" to be integers."""
return "(%s)" % ",".join([str(i) for i in ids]) return "(%s)" % ",".join([str(i) for i in ids])
def safeClassName(name):
return name
def checksum(data):
return md5(data).hexdigest()

View file

@ -1,6 +1,6 @@
# coding: utf-8 # coding: utf-8
import nose, os import nose, os, re
from tests.shared import assertException from tests.shared import assertException
from anki.errors import * from anki.errors import *
@ -67,7 +67,7 @@ def test_saveAs():
deck.addFact(f) deck.addFact(f)
# save in new deck # save in new deck
newDeck = deck.saveAs(path) newDeck = deck.saveAs(path)
assert newDeck.cardCount() == 1 assert newDeck.cardCount == 1
newDeck.close() newDeck.close()
deck.close() deck.close()
@ -96,7 +96,7 @@ def test_factAddDelete():
assert len(f.cards) == 2 assert len(f.cards) == 2
# ensure correct order # ensure correct order
c0 = [c for c in f.cards if c.ordinal == 0][0] c0 = [c for c in f.cards if c.ordinal == 0][0]
assert c0.question == u"one" assert re.sub("</?.+?>", "", c0.question) == u"one"
# now let's make a duplicate # now let's make a duplicate
f2 = deck.newFact() f2 = deck.newFact()
f2['Front'] = u"one"; f2['Back'] = u"three" f2['Front'] = u"one"; f2['Back'] = u"three"
@ -133,7 +133,7 @@ def test_modelAddDelete():
f['Expression'] = u'1' f['Expression'] = u'1'
f['Meaning'] = u'2' f['Meaning'] = u'2'
deck.addFact(f) deck.addFact(f)
assert deck.cardCount() == 2 assert deck.cardCount == 2
deck.deleteModel(deck.currentModel) deck.deleteModel(deck.currentModel)
assert deck.cardCount() == 0 assert deck.cardCount == 0
deck.s.refresh(deck) deck.s.refresh(deck)

View file

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

View file

@ -5,7 +5,7 @@ from tests.shared import assertException
from anki.errors import * from anki.errors import *
from anki import DeckStorage from anki import DeckStorage
from anki.importing import anki03, anki10, csv, mnemosyne10 from anki.importing import anki10, csv, mnemosyne10
from anki.stdmodels import BasicModel from anki.stdmodels import BasicModel
from anki.db import * from anki.db import *
@ -32,15 +32,6 @@ def test_mnemosyne10():
assert i.total == 5 assert i.total == 5
deck.s.close() deck.s.close()
def test_anki03():
deck = DeckStorage.Deck()
file = unicode(os.path.join(testDir, "importing/test03.anki"))
i = anki03.Anki03Importer(deck, file)
i.doImport()
assert len(i.log) == 0
assert i.total == 2
deck.s.close()
def test_anki10(): def test_anki10():
# though these are not modified, sqlite updates the mtime, so copy to tmp # though these are not modified, sqlite updates the mtime, so copy to tmp
# first # first

View file

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