diff --git a/anki/cards.py b/anki/cards.py
index bbe6c612a..a1aace277 100644
--- a/anki/cards.py
+++ b/anki/cards.py
@@ -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 = '''
%s
''' % (
+ 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 (("")
+
+ 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 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):
- alltags = parseTags(self.tags + "," +
- self.fact.tags + "," +
- self.cardModel.name + "," +
- self.fact.model.tags)
- return findTag(tag, alltags)
+ 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
##########################################################################
diff --git a/anki/db.py b/anki/db.py
index d7f3374ef..e65336da2 100644
--- a/anki/db.py
+++ b/anki/db.py
@@ -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)
diff --git a/anki/deck.py b/anki/deck.py
index fa7d62ffe..3400613cd 100644
--- a/anki/deck.py
+++ b/anki/deck.py
@@ -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)
- else:
- break
- return arr
+ def getCards(self):
+ sel = """
+select id, factId, modified, question, answer, cardModelId,
+reps, successive from """
+ if self.newCardOrder == 0:
+ new = "acqCardsRandom"
+ else:
+ 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 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 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]
- if pend:
- self.s.execute("""
+ # 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)
diff --git a/anki/exporting.py b/anki/exporting.py
index 8ca700e09..7705ee246 100644
--- a/anki/exporting.py
+++ b/anki/exporting.py
@@ -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()
diff --git a/anki/facts.py b/anki/facts.py
index 22c720586..fd3faf89e 100644
--- a/anki/facts.py
+++ b/anki/facts.py
@@ -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
diff --git a/anki/importing/__init__.py b/anki/importing/__init__.py
index b5d928c80..10976b746 100644
--- a/anki/importing/__init__.py
+++ b/anki/importing/__init__.py
@@ -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):
diff --git a/anki/importing/anki03.py b/anki/importing/anki03.py
deleted file mode 100644
index 9a3c5ad16..000000000
--- a/anki/importing/anki03.py
+++ /dev/null
@@ -1,285 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright: Damien Elmes
-# 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()
diff --git a/anki/media.py b/anki/media.py
index 99d5d2c69..281ce25a6 100644
--- a/anki/media.py
+++ b/anki/media.py
@@ -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())
diff --git a/anki/models.py b/anki/models.py
index 816cd76ba..d2fc82e27 100644
--- a/anki/models.py
+++ b/anki/models.py
@@ -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", "
")
- htmlFields[k] = '%s' % (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 = '%s
' % (type, html)
- attr = type + 'Align'
- if getattr(self, attr) == 0:
- align = "center"
- elif getattr(self, attr) == 1:
- align = "left"
- else:
- align = "right"
- html = (("")
- 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] = '%s' % (
+ 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
##########################################################################
diff --git a/anki/stats.py b/anki/stats.py
index 253e8bcbf..df6c08107 100644
--- a/anki/stats.py
+++ b/anki/stats.py
@@ -323,8 +323,8 @@ class DeckStats(object):
d = self.deck
html="" + _("Deck Statistics") + "
"
html += _("Deck created: %s ago
") % 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: %(gNewYes%)0.1f%% "
"(%(gNewYes)d of %(gNewTotal)d)
") % stats
# average pending time
- existing = d.cardCount() - d.newCardCount()
+ existing = d.cardCount - d.newTodayCount
avgInt = self.getAverageInterval()
def tr(a, b):
return "| %s | %s |
" % (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)
diff --git a/anki/sync.py b/anki/sync.py
index 1d5449df3..fce1dc334 100644
--- a/anki/sync.py
+++ b/anki/sync.py
@@ -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,
diff --git a/anki/utils.py b/anki/utils.py
index 24f9a63bd..c0ab7718f 100644
--- a/anki/utils.py
+++ b/anki/utils.py
@@ -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()
diff --git a/tests/test_deck.py b/tests/test_deck.py
index 12e1bf959..ec0f8cb00 100644
--- a/tests/test_deck.py
+++ b/tests/test_deck.py
@@ -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)
diff --git a/tests/test_exporting.py b/tests/test_exporting.py
index 4638422e0..295309618 100644
--- a/tests/test_exporting.py
+++ b/tests/test_exporting.py
@@ -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():
diff --git a/tests/test_importing.py b/tests/test_importing.py
index a614fa510..2f7026662 100644
--- a/tests/test_importing.py
+++ b/tests/test_importing.py
@@ -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
diff --git a/tests/test_sync.py b/tests/test_sync.py
index b0dfcb8b6..bd54ece2c 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -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"