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
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.utils import parseTags, findTag, stripHTML, genID
from anki.utils import parseTags, findTag, stripHTML, genID, hexifyID
# Cards
##########################################################################
@ -58,7 +58,7 @@ cardsTable = Table(
# data to the above
Column('yesCount', Integer, nullable=False, default=0),
Column('noCount', Integer, nullable=False, default=0),
# cache
# caching
Column('spaceUntil', Float, nullable=False, default=0),
Column('relativeDelay', Float, nullable=False, default=0),
Column('isDue', Boolean, nullable=False, default=0),
@ -81,13 +81,12 @@ class Card(object):
if cardModel:
self.cardModel = cardModel
self.ordinal = cardModel.ordinal
self.question = cardModel.renderQA(self, self.fact, "question")
self.answer = cardModel.renderQA(self, self.fact, "answer")
htmlQuestion = property(lambda self: self.cardModel.renderQA(
self, self.fact, "question", format="html"))
htmlAnswer = property(lambda self: self.cardModel.renderQA(
self, self.fact, "answer", format="html"))
d = {}
for f in self.fact.model.fieldModels:
d[f.name] = (f.id, self.fact[f.name])
qa = formatQA(None, fact.modelId, d, self.allTags(), cardModel)
self.question = qa['question']
self.answer = qa['answer']
def setModified(self):
self.modified = time.time()
@ -104,13 +103,27 @@ class Card(object):
def totalTime(self):
return time.time() - self.timerStarted
def css(self):
return self.cardModel.css() + self.fact.css()
def genFuzz(self):
"Generate a random offset to spread intervals."
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):
self.reps += 1
if ease > 1:
@ -139,12 +152,15 @@ class Card(object):
self.firstAnswered = time.time()
self.setModified()
def hasTag(self, tag):
alltags = parseTags(self.tags + "," +
def allTags(self):
"Non-canonified string of all tags."
return (self.tags + "," +
self.fact.tags + "," +
self.cardModel.name + "," +
self.fact.model.tags)
return findTag(tag, alltags)
def hasTag(self, tag):
return findTag(tag, parseTags(self.allTags()))
def fromDB(self, s, id):
r = s.first("select * from cards where id = :id",
@ -245,6 +261,7 @@ mapper(Fact, factsTable, properties={
cardsTable.c.id == factsTable.c.lastCardId),
})
# Card deletions
##########################################################################

View file

@ -72,11 +72,11 @@ class SessionHelper(object):
def statement(self, sql, **kwargs):
"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):
"Execute a statement across data. Flush first."
self.execute(text(sql), data)
return self.execute(text(sql), data)
def __repr__(self):
return repr(self._session)

View file

@ -8,16 +8,21 @@ The Deck
"""
__docformat__ = 'restructuredtext'
# - rebuildqueue compuls.
import tempfile, time, os, random, sys, re, stat, shutil, types
from anki.db import *
from anki.lang import _
from anki.errors import DeckAccessError, DeckWrongFormatError
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.models import Model, CardModel
from anki.models import Model, CardModel, formatQA
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
import anki.models, anki.facts, anki.cards, anki.stats
@ -41,7 +46,7 @@ decksTable = Table(
Column('created', Float, nullable=False, default=time.time),
Column('modified', Float, nullable=False, default=time.time),
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")),
# syncing
Column('syncName', UnicodeText),
@ -78,7 +83,14 @@ decksTable = Table(
Column('sessionRepLimit', Integer, nullable=False, default=100),
Column('sessionTimeLimit', Integer, nullable=False, default=1800),
# 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):
"Top-level object. Manages facts, cards and scheduling information."
@ -98,7 +110,6 @@ class Deck(object):
def _initVars(self):
self.lastTags = u""
self.lastLoaded = time.time()
self._countsDirty = True
def modifiedSinceSave(self):
return self.modified > self.lastLoaded
@ -108,30 +119,20 @@ class Deck(object):
def getCard(self, orm=True):
"Return the next card object, or None."
self.checkDue()
id = self.getCardId()
if id:
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):
"Return the next due card id, or None."
now = time.time()
ids = []
# failed card due?
id = self.s.scalar("select id from failedCardsNow limit 1")
if id:
return id
if self.failedNowCount:
return self.s.scalar("select id from failedCardsNow limit 1")
# failed card queue too big?
if self.failedCount >= self.failedCardMax:
id = self.s.scalar(
if self.failedSoonCount >= self.failedCardMax:
return self.s.scalar(
"select id from failedCardsSoon limit 1")
if id:
return id
# distribute new cards?
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
if self._timeForNewCard():
@ -139,9 +140,8 @@ Caller must ensure multiple cards of the same fact are not shown."""
if id:
return id
# card due for review?
id = self.s.scalar("select id from revCards limit 1")
if id:
return id
if self.revCount:
return self.s.scalar("select id from revCards limit 1")
# new card last?
if self.newCardSpacing == NEW_CARDS_LAST:
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")
return id
def getCardIds(self, limit):
"""Return limit number of cards.
Caller is responsible for ensuring cards are not spaced."""
def getCard():
id = self.getCardId()
self.s.statement("update cards set isDue = 0 where id = :id", id=id)
return id
arr = []
for i in range(limit):
c = getCard()
if c:
arr.append(c)
def getCards(self):
sel = """
select id, factId, modified, question, answer, cardModelId,
reps, successive from """
if self.newCardOrder == 0:
new = "acqCardsRandom"
else:
break
return arr
new = "acqCardsOrdered"
return {'failed': self.s.all(sel + "failedCardsNow limit 30"),
'rev': self.s.all(sel + "revCards limit 30"),
'new': self.s.all(sel + new + " limit 30")}
# Get card: helper functions
##########################################################################
@ -175,7 +171,7 @@ Caller is responsible for ensuring cards are not spaced."""
def _timeForNewCard(self):
"True if it's time to display a new card when distributing."
# no cards for review, so force new
if not self.reviewCount:
if not self.revCount:
return True
# force old if there are very high priority cards
if self.s.scalar(
@ -189,7 +185,8 @@ Caller is responsible for ensuring cards are not spaced."""
def _maybeGetNewCard(self):
"Get a new card, provided daily new card limit not exceeded."
if not self.newCountLeftToday:
if not self.newCountToday:
print "no count"
return
return self._getNewCard()
@ -221,6 +218,7 @@ Caller is responsible for ensuring cards are not spaced."""
##########################################################################
def answerCard(self, card, ease):
t = time.time()
self.checkDailyStats()
now = time.time()
oldState = self.cardState(card)
@ -233,8 +231,7 @@ Caller is responsible for ensuring cards are not spaced."""
card.isDue = 0
card.lastFactor = card.factor
self.updateFactor(card, ease)
# spacing - first, we get the times of all other cards with the same
# fact
# spacing
(minSpacing, spaceFactor) = self.s.first("""
select models.initialSpacing, models.spacing from
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 = max(minSpacing, space)
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("""
update cards set
spaceUntil = :space,
@ -267,10 +280,7 @@ where id != :id and factId = :factId""",
entry = CardHistoryEntry(card, ease, lastDelay)
entry.writeSQL(self.s)
self.modified = now
# update isDue for failed cards
self.markExpiredCardsDue()
# invalidate counts
self._countsDirty = True
#print "ans", time.time() - t
# Queue/cache management
##########################################################################
@ -287,36 +297,67 @@ then 1 -- review
else 2 -- new
end)""" + where)
def markExpiredCardsDue(self):
"Mark expired cards due, and update their relativeDelay."
self.s.statement("""update cards
set isDue = 1, relativeDelay = interval / (strftime("%s", "now") - due + 1)
where isDue = 0 and priority in (1,2,3,4) and combinedDue < :now""",
now=time.time())
def rebuildCounts(self):
t = time.time()
self.cardCount = self.s.scalar("select count(*) from cards")
self.factCount = self.s.scalar("select count(*) from facts")
self.failedNowCount = self.s.scalar(
"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):
"Update relative delays for expired cards."
self.s.statement("""update cards
set relativeDelay = interval / (strftime("%s", "now") - due + 1)
where isDue = 1 and type = 1""")
def checkDue(self):
"Mark expired cards due, and update counts."
t2 = time.time()
t = time.time()
# 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."
if updateRelative:
self.updateRelativeDelays()
self.markExpiredCardsDue()
# cache global/daily stats
t = time.time()
# setup global/daily stats
self._globalStats = globalStats(self)
self._dailyStats = dailyStats(self)
# mark due cards and update counts
self.checkDue()
# invalid card count
self._countsDirty = True
# determine new card distribution
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
if self.newCountLeftToday:
self.newCardModulus = ((self.newCountLeftToday + self.reviewCount)
/ self.newCountLeftToday)
if self.newCountToday:
self.newCardModulus = (
(self.newCountToday + self.revCount) / self.newCountToday)
# if there are cards to review, ensure modulo >= 2
if self.reviewCount:
if self.revCount:
self.newCardModulus = max(2, self.newCardModulus)
else:
self.newCardModulus = 0
@ -324,6 +365,9 @@ where isDue = 1 and type = 1""")
self.averageFactor = (self.s.scalar(
"select avg(factor) from cards where type = 1")
or Deck.initialFactor)
# recache css
self.rebuildCSS()
#print "rebuild queue", time.time() - t
def checkDailyStats(self):
# check if the day has rolled over
@ -336,8 +380,11 @@ where isDue = 1 and type = 1""")
def nextInterval(self, card, ease):
"Return the next interval for CARD given 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 card.interval < self.hardIntervalMin:
if interval < self.hardIntervalMin:
if ease < 2:
interval = NEW_INTERVAL
elif ease == 2:
@ -354,19 +401,19 @@ where isDue = 1 and type = 1""")
else:
# otherwise, multiply the old interval by a factor
if ease == 1:
factor = 1 / card.factor / 2.0
interval = card.interval * factor
factor = 1 / factor / 2.0
interval = interval * factor
elif ease == 2:
factor = 1.2
interval = (card.interval + delay/4) * factor
interval = (interval + delay/4) * factor
elif ease == 3:
factor = card.factor
interval = (card.interval + delay/2) * factor
factor = factor
interval = (interval + delay/2) * factor
elif ease == 4:
factor = card.factor * self.factorFour
interval = (card.interval + delay) * factor
assert card.fuzz
interval *= card.fuzz
factor = factor * self.factorFour
interval = (interval + delay) * factor
fuzz = random.uniform(0.95, 1.05)
interval *= fuzz
if self.maxScheduleTime:
interval = min(interval, self.maxScheduleTime)
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.
Ignore new cards."""
return self.s.scalar("""
select combinedDue from cards where priority != 0 and type != 2
order by combinedDue limit 1""")
select combinedDue from cards where priority in (1,2,3,4) and
type in (0, 1) order by combinedDue limit 1""")
def earliestTimeStr(self, next=None):
"""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"
return self.s.scalar("""
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):
"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
##########################################################################
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):
return self.s.scalar(
"select count(id) from cards where priority = 0")
return self.s.scalar("""
select count(id) from cards where type in (0,1,2) and isDue in (0,1)
and priority = 0""")
def seenCardCount(self):
return self.s.scalar(
"select count(id) from cards where reps != 0")
def newCardCount(self):
return self.s.scalar(
"select count(id) from cards where reps = 0")
"select count(id) from cards where type in (0, 1)")
# Counts related to due cards
##########################################################################
@ -605,56 +641,14 @@ suspended</a> cards.''') % {
self._dailyStats.newEase3 +
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):
return self.s.scalar("""
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())
def isEmpty(self):
return self.cardCount() == 0
return not self.cardCount
def matureCardCount(self):
return self.s.scalar(
@ -699,9 +693,9 @@ priority != 0 and due < :now and spaceUntil > :now""",
"Return some commonly needed stats."
stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats)
# add scheduling related stats
stats['new'] = self.newCountLeftToday
stats['failed'] = self.failedCount
stats['successive'] = self.reviewCount
stats['new'] = self.newCountToday
stats['failed'] = self.failedSoonCount
stats['successive'] = self.revCount
if stats['dAverageTime']:
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
count = stats['successive'] + stats['new']
@ -725,9 +719,6 @@ priority != 0 and due < :now and spaceUntil > :now""",
return "failed"
elif card.reps:
return "rev"
else:
sys.stderr.write("couldn't determine queue for %s" %
`card.__dict__`)
# Facts
##########################################################################
@ -754,16 +745,18 @@ priority != 0 and due < :now and spaceUntil > :now""",
cards = []
self.refresh()
self.s.save(fact)
self.factCount += 1
self.flushMod()
for cardModel in cms:
card = anki.cards.Card(fact, cardModel)
self.flushMod()
self.updatePriority(card)
cards.append(card)
self.cardCount += len(cards)
self.newCount += len(cards)
# keep track of last used tags for convenience
self.lastTags = fact.tags
self.setModified()
self._countsDirty = True
return cards
def availableCardModels(self, fact):
@ -822,9 +815,11 @@ where factId = :fid and cardModelId = :cmid""",
self.s.statement("insert into cardsDeleted select id, :time "
"from cards where 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
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("insert into factsDeleted values (:id, :time)",
id=factId, time=time.time())
@ -837,7 +832,8 @@ where factId = :fid and cardModelId = :cmid""",
self.s.flush()
now = time.time()
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)
data = [{'id': id, 'time': now} for id in ids]
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."
self.s.flush()
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)",
id=id, time=time.time())
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"
% strids)
# 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
data = [{'id': id, 'time': now} for id in ids]
self.s.statements("insert into cardsDeleted values (:id, :time)", data)
@ -1000,6 +998,33 @@ fieldModelId = :new where fieldModelId = :old""",
self.deleteModel(model)
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
##########################################################################
@ -1089,35 +1114,45 @@ cardModelId = :id""", id=cardModel.id)
model.setModified()
self.flushMod()
def updateCardsFromModel(self, cardModel):
def updateCardsFromModel(self, model):
"Update all card question/answer when model changes."
ids = self.s.all("""
select cards.id, cards.cardModelId, cards.factId from
cards, facts, cardmodels where
select cards.id, cards.cardModelId, cards.factId, facts.modelId from
cards, facts where
cards.factId = facts.id and
facts.modelId = cardModels.modelId and
cards.cardModelId = :id""", id=cardModel.id)
facts.modelId = :id""", id=model.id)
if not ids:
return
self.updateCardQACache(ids)
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:
mod = ", modified = %f" % time.time()
else:
mod = ""
cms = self.s.query(CardModel).all()
for cm in cms:
pend = [{'q': cm.renderQASQL('q', fid),
'a': cm.renderQASQL('a', fid),
'id': cid}
for (cid, cmid, fid) in ids if cmid == cm.id]
# tags
tags = dict(self.tagsList(priority="", where="and cards.id in %s" %
ids2str([x[0] for x in ids])))
facts = {}
# fields
for k, g in groupby(self.s.all("""
select fields.factId, fieldModels.name, fieldModels.id, fields.value
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
question = :q,
answer = :a
question = :question, answer = :answer
%s
where id = :id""" % mod, pend)
@ -1135,13 +1170,13 @@ where cardModelId in %s""" % strids, now=time.time())
# Tags
##########################################################################
def tagsList(self, where=""):
def tagsList(self, where="", priority=", cards.priority"):
"Return a list of (cardId, allTags, priority)"
return self.s.all("""
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
and cards.cardModelId = cardModels.id %s""" % where)
and cards.cardModelId = cardModels.id %s""" % (priority, where))
def allTags(self):
"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",
newFields)
# regenerate question/answer cache
cms = self.s.query(CardModel).all()
for cm in cms:
self.updateCardsFromModel(cm)
self.s.expunge(cm)
for m in self.models:
self.updateCardsFromModel(m)
# forget all deletions
self.s.statement("delete from cardsDeleted")
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 models set modified = :t", t=time.time())
self.lastSync = 0
# update counts
self.rebuildCounts()
# update deck and save
self.flushMod()
self.save()
@ -1537,6 +1572,19 @@ alter table decks add column sessionTimeLimit integer not null default 1800""")
if ver < 11:
s.execute("""
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)
# attach db vars
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."
# card queues
deck.s.statement("""
create index if not exists ix_cards_markExpired on cards
(isDue, priority desc, combinedDue desc)""")
deck.s.statement("""
create index if not exists ix_cards_failedIsDue on cards
(type, isDue, combinedDue)""")
create index if not exists ix_cards_checkDueOrder on cards
(type, isDue, priority desc, combinedDue desc)""")
deck.s.statement("""
create index if not exists ix_cards_failedOrder on cards
(type, isDue, due)""")
(type, isDue, priority desc, due)""")
deck.s.statement("""
create index if not exists ix_cards_revisionOrder on cards
(type, isDue, priority desc, relativeDelay)""")
(type, isDue, priority desc, interval desc)""")
deck.s.statement("""
create index if not exists ix_cards_newRandomOrder on cards
(priority desc, factId, ordinal)""")
(type, isDue, priority desc, factId, ordinal)""")
deck.s.statement("""
create index if not exists ix_cards_newOrderedOrder on cards
(priority desc, due)""")
(type, isDue, priority desc, due)""")
# card spacing
deck.s.statement("""
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 failedCards")
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("""
create view failedCardsNow as
select * from cards
where type = 0 and isDue = 1
and combinedDue <= (strftime("%s", "now") + 1)
order by combinedDue
order by due
""")
s.statement("drop view if exists failedCardsSoon")
s.statement("""
create view failedCardsSoon as
select * from cards
where type = 0 and priority != 0
and combinedDue <=
(select max(delay0, delay1)+strftime("%s", "now")+1
from decks)
where type = 0 and priority in (1,2,3,4)
and combinedDue <= (select max(delay0, delay1) +
strftime("%s", "now")+1 from decks)
order by modified
""")
s.statement("drop view if exists revCards")
s.statement("""
create view revCards as
select * from cards where
type = 1 and isDue = 1
order by type, isDue, priority desc, relativeDelay""")
s.statement("drop view if exists acqCardsRandom")
select * from cards
where type = 1 and isDue = 1
order by priority desc, interval desc""")
s.statement("""
create view acqCardsRandom as
select * from cards
where type = 2 and isDue = 1
order by priority desc, factId, ordinal""")
s.statement("drop view if exists acqCardsOrdered")
s.statement("""
create view acqCardsOrdered as
select * from cards
@ -1838,6 +1881,24 @@ alter table models add column source integer not null default 0""")
DeckStorage._setUTCOffset(deck)
deck.version = 11
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
_upgradeDeck = staticmethod(_upgradeDeck)

View file

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

View file

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

View file

@ -123,6 +123,7 @@ all but one card model."""))
[{'modelId': self.model.id,
'tags': self.tagsToAdd,
'id': factIds[n]} for n in range(len(cards))])
self.deck.factCount += len(factIds)
self.deck.s.execute("""
delete from factsDeleted
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],
'cardModelId': cm.id,
'ordinal': cm.ordinal,
'question': cm.renderQASQL('q', factIds[m]),
'answer': cm.renderQASQL('a', factIds[m]),
'question': u"",
'answer': u"",
'type': 2},cards[m]) for m in range(len(cards))]
self.deck.s.execute(cardsTable.insert(),
data)
self.deck.updateCardsFromModel(self.model)
self.deck.cardCount += len(cards)
self.total = len(factIds)
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'
try:
import hashlib
md5 = hashlib.md5
except ImportError:
import md5
md5 = md5.new
import os, stat, time, shutil, re
from anki.db import *
from anki.facts import Fact
from anki.utils import addTags, genID, ids2str
from anki.utils import addTags, genID, ids2str, checksum
from anki.lang import _
regexps = (("(\[sound:([^]]+)\])",
@ -52,9 +45,6 @@ mediaDeletedTable = Table(
# Helper functions
##########################################################################
def checksum(data):
return md5(data).hexdigest()
def mediaFilename(path):
"Return checksum.ext for path"
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
from sqlalchemy.ext.orderinglist import ordering_list
from anki.db import *
from anki.utils import genID
from anki.utils import genID, canonifyTags, safeClassName
from anki.fonts import toPlatformFont
from anki.utils import parseTags
from anki.utils import parseTags, hexifyID, checksum
from anki.lang import _
from copy import copy
def alignmentLabels():
return {
@ -58,18 +59,6 @@ class FieldModel(object):
self.unique = unique
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)
# Card models
@ -119,81 +108,28 @@ class CardModel(object):
self.active = active
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)
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
##########################################################################

View file

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

View file

@ -425,8 +425,13 @@ where factId in %s""" % factIds))
'spaceUntil': f[5],
'lastCardId': f[6]
} 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("""
insert or replace into facts
insert or ignore into facts
(id, modelId, created, modified, tags, spaceUntil, lastCardId)
values
(:id, :modelId, :created, :modified, :tags, :spaceUntil, :lastCardId)""", dlist)
@ -509,6 +514,11 @@ from cards where id in %s""" % ids2str(ids)))
'type': c[35],
'combinedDue': c[36],
} 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("""
insert or replace into cards
(id, factId, cardModelId, created, modified, tags, ordinal,
@ -782,6 +792,9 @@ where media.id in %s""" % sids, now=time.time())
t = time.time()
dlist = [{'id': c[0], 'factId': c[1], 'cardModelId': c[2],
'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
self.deck.s.statements("""
insert or ignore into cards
@ -800,7 +813,15 @@ values
0, 0, 0, 0, 0,
0, "", "", 2.5, 0, 1, 2, :t, 0)""", dlist)
# 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
##########################################################################
@ -972,7 +993,7 @@ class HttpSyncServer(SyncServer):
return self.stuff(SyncServer.genOneWayPayload(self,
self.unstuff(payload)))
def getDecks(self, libanki, client, sources):
def getDecks(self, libanki, client, sources, pversion):
return self.stuff({
"status": "OK",
"decks": self.decks,

View file

@ -10,6 +10,13 @@ __docformat__ = 'restructuredtext'
import re, os, random, time
try:
import hashlib
md5 = hashlib.md5
except ImportError:
import md5
md5 = md5.new
from anki.db import *
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
to be integers."""
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
import nose, os
import nose, os, re
from tests.shared import assertException
from anki.errors import *
@ -67,7 +67,7 @@ def test_saveAs():
deck.addFact(f)
# save in new deck
newDeck = deck.saveAs(path)
assert newDeck.cardCount() == 1
assert newDeck.cardCount == 1
newDeck.close()
deck.close()
@ -96,7 +96,7 @@ def test_factAddDelete():
assert len(f.cards) == 2
# ensure correct order
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
f2 = deck.newFact()
f2['Front'] = u"one"; f2['Back'] = u"three"
@ -133,7 +133,7 @@ def test_modelAddDelete():
f['Expression'] = u'1'
f['Meaning'] = u'2'
deck.addFact(f)
assert deck.cardCount() == 2
assert deck.cardCount == 2
deck.deleteModel(deck.currentModel)
assert deck.cardCount() == 0
assert deck.cardCount == 0
deck.s.refresh(deck)

View file

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

View file

@ -5,7 +5,7 @@ from tests.shared import assertException
from anki.errors import *
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.db import *
@ -32,15 +32,6 @@ def test_mnemosyne10():
assert i.total == 5
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():
# though these are not modified, sqlite updates the mtime, so copy to tmp
# first

View file

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