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 (("
" % align) + + div + "
") + + 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 = (("
" % align) + - 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"