mirror of
https://github.com/ankitects/anki.git
synced 2025-12-13 23:00:58 -05:00
wip
This commit is contained in:
parent
83bc433e19
commit
b2d0e5d3df
16 changed files with 393 additions and 643 deletions
|
|
@ -10,9 +10,9 @@ __docformat__ = 'restructuredtext'
|
|||
|
||||
import time, sys, math, random
|
||||
from anki.db import *
|
||||
from anki.models import CardModel, Model, FieldModel
|
||||
from anki.models import CardModel, Model, FieldModel, formatQA
|
||||
from anki.facts import Fact, factsTable, Field
|
||||
from anki.utils import parseTags, findTag, stripHTML, genID
|
||||
from anki.utils import parseTags, findTag, stripHTML, genID, hexifyID
|
||||
|
||||
# Cards
|
||||
##########################################################################
|
||||
|
|
@ -58,7 +58,7 @@ cardsTable = Table(
|
|||
# data to the above
|
||||
Column('yesCount', Integer, nullable=False, default=0),
|
||||
Column('noCount', Integer, nullable=False, default=0),
|
||||
# cache
|
||||
# caching
|
||||
Column('spaceUntil', Float, nullable=False, default=0),
|
||||
Column('relativeDelay', Float, nullable=False, default=0),
|
||||
Column('isDue', Boolean, nullable=False, default=0),
|
||||
|
|
@ -81,13 +81,12 @@ class Card(object):
|
|||
if cardModel:
|
||||
self.cardModel = cardModel
|
||||
self.ordinal = cardModel.ordinal
|
||||
self.question = cardModel.renderQA(self, self.fact, "question")
|
||||
self.answer = cardModel.renderQA(self, self.fact, "answer")
|
||||
|
||||
htmlQuestion = property(lambda self: self.cardModel.renderQA(
|
||||
self, self.fact, "question", format="html"))
|
||||
htmlAnswer = property(lambda self: self.cardModel.renderQA(
|
||||
self, self.fact, "answer", format="html"))
|
||||
d = {}
|
||||
for f in self.fact.model.fieldModels:
|
||||
d[f.name] = (f.id, self.fact[f.name])
|
||||
qa = formatQA(None, fact.modelId, d, self.allTags(), cardModel)
|
||||
self.question = qa['question']
|
||||
self.answer = qa['answer']
|
||||
|
||||
def setModified(self):
|
||||
self.modified = time.time()
|
||||
|
|
@ -104,13 +103,27 @@ class Card(object):
|
|||
def totalTime(self):
|
||||
return time.time() - self.timerStarted
|
||||
|
||||
def css(self):
|
||||
return self.cardModel.css() + self.fact.css()
|
||||
|
||||
def genFuzz(self):
|
||||
"Generate a random offset to spread intervals."
|
||||
self.fuzz = random.uniform(0.95, 1.05)
|
||||
|
||||
def htmlQuestion(self, type="question"):
|
||||
div = '''<div id="cm%s%s">%s</div>''' % (
|
||||
type[0], hexifyID(self.cardModel.id), getattr(self, type))
|
||||
# add outer div & alignment (with tables due to qt's html handling)
|
||||
attr = type + 'Align'
|
||||
if getattr(self.cardModel, attr) == 0:
|
||||
align = "center"
|
||||
elif getattr(self.cardModel, attr) == 1:
|
||||
align = "left"
|
||||
else:
|
||||
align = "right"
|
||||
return (("<center><table width=95%%><tr><td align=%s>" % align) +
|
||||
div + "</td></tr></table></center>")
|
||||
|
||||
def htmlAnswer(self):
|
||||
return self.htmlQuestion(type="answer")
|
||||
|
||||
def updateStats(self, ease, state):
|
||||
self.reps += 1
|
||||
if ease > 1:
|
||||
|
|
@ -139,12 +152,15 @@ class Card(object):
|
|||
self.firstAnswered = time.time()
|
||||
self.setModified()
|
||||
|
||||
def 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
|
||||
##########################################################################
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
453
anki/deck.py
453
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</a> cards.''') % {
|
|||
# Card/fact counts - all in deck, not just due
|
||||
##########################################################################
|
||||
|
||||
def cardCount(self):
|
||||
return self.s.scalar(
|
||||
"select count(id) from cards")
|
||||
|
||||
def factCount(self):
|
||||
return self.s.scalar(
|
||||
"select count(id) from facts")
|
||||
|
||||
def suspendedCardCount(self):
|
||||
return self.s.scalar(
|
||||
"select count(id) from cards where priority = 0")
|
||||
return self.s.scalar("""
|
||||
select count(id) from cards where type in (0,1,2) and isDue in (0,1)
|
||||
and priority = 0""")
|
||||
|
||||
def seenCardCount(self):
|
||||
return self.s.scalar(
|
||||
"select count(id) from cards where reps != 0")
|
||||
|
||||
def newCardCount(self):
|
||||
return self.s.scalar(
|
||||
"select count(id) from cards where reps = 0")
|
||||
"select count(id) from cards where type in (0, 1)")
|
||||
|
||||
# Counts related to due cards
|
||||
##########################################################################
|
||||
|
|
@ -605,56 +641,14 @@ suspended</a> cards.''') % {
|
|||
self._dailyStats.newEase3 +
|
||||
self._dailyStats.newEase4)
|
||||
|
||||
def updateCounts(self):
|
||||
"Update failed/rev/new counts if cache is dirty."
|
||||
if self._countsDirty:
|
||||
self._failedCount = self.s.scalar("""
|
||||
select count(id) from failedCardsSoon""")
|
||||
self._failedDueNowCount = self.s.scalar("""
|
||||
select count(id) from failedCardsNow""")
|
||||
self._reviewCount = self.s.scalar(
|
||||
"select count(isDue) from cards where isDue = 1 and type = 1")
|
||||
self._newCount = self.s.scalar(
|
||||
"select count(isDue) from cards where isDue = 1 and type = 2")
|
||||
if getattr(self, '_dailyStats', None):
|
||||
self._newCountLeftToday = max(min(
|
||||
self._newCount, self.newCardsPerDay -
|
||||
self.newCardsToday()), 0)
|
||||
self._countsDirty = False
|
||||
|
||||
def _getFailedCount(self):
|
||||
self.updateCounts()
|
||||
return self._failedCount
|
||||
failedCount = property(_getFailedCount)
|
||||
|
||||
def _getFailedDueNowCount(self):
|
||||
self.updateCounts()
|
||||
return self._failedDueNowCount
|
||||
failedDueNowCount = property(_getFailedDueNowCount)
|
||||
|
||||
def _getReviewCount(self):
|
||||
self.updateCounts()
|
||||
return self._reviewCount
|
||||
reviewCount = property(_getReviewCount)
|
||||
|
||||
def _getNewCount(self):
|
||||
self.updateCounts()
|
||||
return self._newCount
|
||||
newCount = property(_getNewCount)
|
||||
|
||||
def _getNewCountLeftToday(self):
|
||||
self.updateCounts()
|
||||
return self._newCountLeftToday
|
||||
newCountLeftToday = property(_getNewCountLeftToday)
|
||||
|
||||
def spacedCardCount(self):
|
||||
return self.s.scalar("""
|
||||
select count(cards.id) from cards where
|
||||
priority != 0 and due < :now and spaceUntil > :now""",
|
||||
type in (1,2) and isDue = 0 and priority in (1,2,3,4) and due < :now""",
|
||||
now=time.time())
|
||||
|
||||
def isEmpty(self):
|
||||
return self.cardCount() == 0
|
||||
return not self.cardCount
|
||||
|
||||
def matureCardCount(self):
|
||||
return self.s.scalar(
|
||||
|
|
@ -699,9 +693,9 @@ priority != 0 and due < :now and spaceUntil > :now""",
|
|||
"Return some commonly needed stats."
|
||||
stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats)
|
||||
# add scheduling related stats
|
||||
stats['new'] = self.newCountLeftToday
|
||||
stats['failed'] = self.failedCount
|
||||
stats['successive'] = self.reviewCount
|
||||
stats['new'] = self.newCountToday
|
||||
stats['failed'] = self.failedSoonCount
|
||||
stats['successive'] = self.revCount
|
||||
if stats['dAverageTime']:
|
||||
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
|
||||
count = stats['successive'] + stats['new']
|
||||
|
|
@ -725,9 +719,6 @@ priority != 0 and due < :now and spaceUntil > :now""",
|
|||
return "failed"
|
||||
elif card.reps:
|
||||
return "rev"
|
||||
else:
|
||||
sys.stderr.write("couldn't determine queue for %s" %
|
||||
`card.__dict__`)
|
||||
|
||||
# Facts
|
||||
##########################################################################
|
||||
|
|
@ -754,16 +745,18 @@ priority != 0 and due < :now and spaceUntil > :now""",
|
|||
cards = []
|
||||
self.refresh()
|
||||
self.s.save(fact)
|
||||
self.factCount += 1
|
||||
self.flushMod()
|
||||
for cardModel in cms:
|
||||
card = anki.cards.Card(fact, cardModel)
|
||||
self.flushMod()
|
||||
self.updatePriority(card)
|
||||
cards.append(card)
|
||||
self.cardCount += len(cards)
|
||||
self.newCount += len(cards)
|
||||
# keep track of last used tags for convenience
|
||||
self.lastTags = fact.tags
|
||||
self.setModified()
|
||||
self._countsDirty = True
|
||||
return cards
|
||||
|
||||
def availableCardModels(self, fact):
|
||||
|
|
@ -822,9 +815,11 @@ where factId = :fid and cardModelId = :cmid""",
|
|||
self.s.statement("insert into cardsDeleted select id, :time "
|
||||
"from cards where factId = :factId",
|
||||
time=time.time(), factId=factId)
|
||||
self.s.statement("delete from cards where factId = :id", id=factId)
|
||||
self.cardCount -= self.s.statement(
|
||||
"delete from cards where factId = :id", id=factId).rowcount
|
||||
# and then the fact
|
||||
self.s.statement("delete from facts where id = :id", id=factId)
|
||||
self.factCount -= self.s.statement(
|
||||
"delete from facts where id = :id", id=factId).rowcount
|
||||
self.s.statement("delete from fields where factId = :id", id=factId)
|
||||
self.s.statement("insert into factsDeleted values (:id, :time)",
|
||||
id=factId, time=time.time())
|
||||
|
|
@ -837,7 +832,8 @@ where factId = :fid and cardModelId = :cmid""",
|
|||
self.s.flush()
|
||||
now = time.time()
|
||||
strids = ids2str(ids)
|
||||
self.s.statement("delete from facts where id in %s" % strids)
|
||||
self.factCount -= self.s.statement(
|
||||
"delete from facts where id in %s" % strids).rowcount
|
||||
self.s.statement("delete from fields where factId in %s" % strids)
|
||||
data = [{'id': id, 'time': now} for id in ids]
|
||||
self.s.statements("insert into factsDeleted values (:id, :time)", data)
|
||||
|
|
@ -858,7 +854,8 @@ where facts.id not in (select factId from cards)""")
|
|||
"Delete a card given its id. Delete any unused facts. Don't flush."
|
||||
self.s.flush()
|
||||
factId = self.s.scalar("select factId from cards where id=:id", id=id)
|
||||
self.s.statement("delete from cards where id = :id", id=id)
|
||||
self.cardCount -= self.s.statement(
|
||||
"delete from cards where id = :id", id=id).rowcount
|
||||
self.s.statement("insert into cardsDeleted values (:id, :time)",
|
||||
id=id, time=time.time())
|
||||
if factId and not self.factUseCount(factId):
|
||||
|
|
@ -876,7 +873,8 @@ where facts.id not in (select factId from cards)""")
|
|||
factIds = self.s.column0("select factId from cards where id in %s"
|
||||
% strids)
|
||||
# drop from cards
|
||||
self.s.statement("delete from cards where id in %s" % strids)
|
||||
self.cardCount -= self.s.statement(
|
||||
"delete from cards where id in %s" % strids).rowcount
|
||||
# note deleted
|
||||
data = [{'id': id, 'time': now} for id in ids]
|
||||
self.s.statements("insert into cardsDeleted values (:id, :time)", data)
|
||||
|
|
@ -1000,6 +998,33 @@ fieldModelId = :new where fieldModelId = :old""",
|
|||
self.deleteModel(model)
|
||||
self.refresh()
|
||||
|
||||
def rebuildCSS(self):
|
||||
# css for all fields
|
||||
def _genCSS(prefix, row):
|
||||
(id, fam, siz, col, align) = row
|
||||
t = ""
|
||||
if fam: t += 'font-family:"%s";' % toPlatformFont(fam)
|
||||
if siz: t += 'font-size:%dpx;' % siz
|
||||
if col: t += 'color:%s;' % col
|
||||
if align != -1:
|
||||
if align == 0: align = "center"
|
||||
elif align == 1: align = "left"
|
||||
else: align = "right"
|
||||
t += 'text-align:%s;' % align
|
||||
if t:
|
||||
t = "%s%s {%s}\n" % (prefix, hexifyID(id), t)
|
||||
return t
|
||||
css = "".join([_genCSS(".fm", row) for row in self.s.all("""
|
||||
select id, quizFontFamily, quizFontSize, quizFontColour, -1 from fieldModels""")])
|
||||
css += "".join([_genCSS("#cmq", row) for row in self.s.all("""
|
||||
select id, questionFontFamily, questionFontSize, questionFontColour,
|
||||
questionAlign from cardModels""")])
|
||||
css += "".join([_genCSS("#cma", row) for row in self.s.all("""
|
||||
select id, answerFontFamily, answerFontSize, answerFontColour,
|
||||
answerAlign from cardModels""")])
|
||||
self.css = css
|
||||
return css
|
||||
|
||||
# Fields
|
||||
##########################################################################
|
||||
|
||||
|
|
@ -1089,35 +1114,45 @@ cardModelId = :id""", id=cardModel.id)
|
|||
model.setModified()
|
||||
self.flushMod()
|
||||
|
||||
def updateCardsFromModel(self, cardModel):
|
||||
def updateCardsFromModel(self, model):
|
||||
"Update all card question/answer when model changes."
|
||||
ids = self.s.all("""
|
||||
select cards.id, cards.cardModelId, cards.factId from
|
||||
cards, facts, cardmodels where
|
||||
select cards.id, cards.cardModelId, cards.factId, facts.modelId from
|
||||
cards, facts where
|
||||
cards.factId = facts.id and
|
||||
facts.modelId = cardModels.modelId and
|
||||
cards.cardModelId = :id""", id=cardModel.id)
|
||||
facts.modelId = :id""", id=model.id)
|
||||
if not ids:
|
||||
return
|
||||
self.updateCardQACache(ids)
|
||||
|
||||
def updateCardQACache(self, ids, dirty=True):
|
||||
"Given a list of (cardId, cardModelId, factId), update q/a cache."
|
||||
"Given a list of (cardId, cardModelId, factId, modId), update q/a cache."
|
||||
if dirty:
|
||||
mod = ", modified = %f" % time.time()
|
||||
else:
|
||||
mod = ""
|
||||
cms = self.s.query(CardModel).all()
|
||||
for cm in cms:
|
||||
pend = [{'q': cm.renderQASQL('q', fid),
|
||||
'a': cm.renderQASQL('a', fid),
|
||||
'id': cid}
|
||||
for (cid, cmid, fid) in ids if cmid == cm.id]
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,285 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
||||
|
||||
"""\
|
||||
Importing Anki v0.3 decks
|
||||
==========================
|
||||
"""
|
||||
__docformat__ = 'restructuredtext'
|
||||
|
||||
import cPickle, cStringIO, os, datetime, types, sys
|
||||
from anki.models import Model, FieldModel, CardModel
|
||||
from anki.facts import Fact, factsTable, fieldsTable
|
||||
from anki.cards import cardsTable
|
||||
from anki.stats import *
|
||||
from anki.features import FeatureManager
|
||||
from anki.importing import Importer
|
||||
from anki.utils import genID
|
||||
|
||||
def transformClasses(m, c):
|
||||
"Map objects into dummy classes"
|
||||
class EmptyClass(object):
|
||||
pass
|
||||
class EmptyList(list):
|
||||
pass
|
||||
class EmptyDict(dict):
|
||||
pass
|
||||
if c == "Fact":
|
||||
return EmptyDict
|
||||
elif c in ("CardModels", "Deck", "Facts", "Fields", "Models"):
|
||||
return EmptyList
|
||||
return EmptyClass
|
||||
|
||||
def load(path):
|
||||
"Load a deck from PATH."
|
||||
if isinstance(path, types.UnicodeType):
|
||||
path = path.encode(sys.getfilesystemencoding())
|
||||
file = open(path, "rb")
|
||||
try:
|
||||
try:
|
||||
data = file.read()
|
||||
except (IOError, OSError):
|
||||
raise ImportError
|
||||
finally:
|
||||
file.close()
|
||||
unpickler = cPickle.Unpickler(cStringIO.StringIO(data))
|
||||
unpickler.find_global = transformClasses
|
||||
deck = unpickler.load()
|
||||
deck.path = unicode(os.path.abspath(path), sys.getfilesystemencoding())
|
||||
return deck
|
||||
|
||||
# we need to upgrade the deck before converting
|
||||
def maybeUpgrade(deck):
|
||||
# change old hardInterval from 1 day to 8-12 hours
|
||||
if list(deck.sched.hardInterval) == [1.0, 1.0]:
|
||||
deck.sched.hardInterval = [0.3333, 0.5]
|
||||
# add 'medium priority' support
|
||||
if not hasattr(deck.sched, 'medPriority'):
|
||||
deck.sched.medPriority = []
|
||||
# add delay2
|
||||
if not hasattr(deck.sched, "delay2"):
|
||||
deck.sched.delay2 = 28800
|
||||
# add collapsing
|
||||
if not hasattr(deck.sched, "collapse"):
|
||||
deck.sched.collapse = 18000
|
||||
# card related
|
||||
# - add 'total' attribute
|
||||
for card in deck:
|
||||
card.__dict__['total'] = (
|
||||
card.stats['new']['yes'] +
|
||||
card.stats['new']['no'] +
|
||||
card.stats['young']['yes'] +
|
||||
card.stats['young']['no'] +
|
||||
card.stats['old']['yes'] +
|
||||
card.stats['old']['no'])
|
||||
|
||||
class Anki03Importer(Importer):
|
||||
|
||||
needMapper = False
|
||||
|
||||
def doImport(self):
|
||||
oldDeck = load(self.file)
|
||||
maybeUpgrade(oldDeck)
|
||||
# mappings for old->new ids
|
||||
cardModels = {}
|
||||
fieldModels = {}
|
||||
# start with the models
|
||||
s = self.deck.s
|
||||
deck = self.deck
|
||||
import types
|
||||
def uni(txt):
|
||||
if txt is None:
|
||||
return txt
|
||||
if not isinstance(txt, types.UnicodeType):
|
||||
txt = unicode(txt, "utf-8")
|
||||
return txt
|
||||
for oldModel in oldDeck.facts.models:
|
||||
model = Model(uni(oldModel.name), uni(oldModel.description))
|
||||
model.id = oldModel.id
|
||||
model.tags = u", ".join(oldModel.tags)
|
||||
model.features = u", ".join(oldModel.decorators)
|
||||
model.created = oldModel.created
|
||||
model.modified = oldModel.modified
|
||||
deck.newCardOrder = min(1, oldModel.position)
|
||||
deck.addModel(model)
|
||||
# fields
|
||||
for oldField in oldModel.fields:
|
||||
fieldModel = FieldModel(uni(oldField.name),
|
||||
uni(oldField.description),
|
||||
oldField.name in oldModel.required,
|
||||
oldField.name in oldModel.unique)
|
||||
fieldModel.features = u", ".join(oldField.features)
|
||||
fieldModel.quizFontFamily = uni(oldField.display['quiz']['fontFamily'])
|
||||
fieldModel.quizFontSize = oldField.display['quiz']['fontSize']
|
||||
fieldModel.quizFontColour = uni(oldField.display['quiz']['fontColour'])
|
||||
fieldModel.editFontFamily = uni(oldField.display['edit']['fontFamily'])
|
||||
fieldModel.editFontSize = oldField.display['edit']['fontSize']
|
||||
fieldModel.id = oldField.id
|
||||
model.addFieldModel(fieldModel)
|
||||
s.flush() # we need the id
|
||||
fieldModels[id(oldField)] = fieldModel
|
||||
# card models
|
||||
for oldCard in oldModel.allcards:
|
||||
cardModel = CardModel(uni(oldCard.name),
|
||||
uni(oldCard.description),
|
||||
uni(oldCard.qformat),
|
||||
uni(oldCard.aformat))
|
||||
cardModel.active = oldCard in oldModel.cards
|
||||
cardModel.questionInAnswer = oldCard.questionInAnswer
|
||||
cardModel.id = oldCard.id
|
||||
model.spacing = 0.25
|
||||
cardModel.questionFontFamily = uni(oldCard.display['question']['fontFamily'])
|
||||
cardModel.questionFontSize = oldCard.display['question']['fontSize']
|
||||
cardModel.questionFontColour = uni(oldCard.display['question']['fontColour'])
|
||||
cardModel.questionAlign = oldCard.display['question']['align']
|
||||
cardModel.answerFontFamily = uni(oldCard.display['answer']['fontFamily'])
|
||||
cardModel.answerFontSize = oldCard.display['answer']['fontSize']
|
||||
cardModel.answerFontColour = uni(oldCard.display['answer']['fontColour'])
|
||||
cardModel.answerAlign = oldCard.display['answer']['align']
|
||||
cardModel.lastFontFamily = uni(oldCard.display['last']['fontFamily'])
|
||||
cardModel.lastFontSize = oldCard.display['last']['fontSize']
|
||||
cardModel.lastFontColour = uni(oldCard.display['last']['fontColour'])
|
||||
cardModel.editQuestionFontFamily = (
|
||||
uni(oldCard.display['editQuestion']['fontFamily']))
|
||||
cardModel.editQuestionFontSize = (
|
||||
oldCard.display['editQuestion']['fontSize'])
|
||||
cardModel.editAnswerFontFamily = (
|
||||
uni(oldCard.display['editAnswer']['fontFamily']))
|
||||
cardModel.editAnswerFontSize = (
|
||||
oldCard.display['editAnswer']['fontSize'])
|
||||
model.addCardModel(cardModel)
|
||||
s.flush() # we need the id
|
||||
cardModels[id(oldCard)] = cardModel
|
||||
# facts
|
||||
def getSpace(lastCard, lastAnswered):
|
||||
if not lastCard:
|
||||
return 0
|
||||
return lastAnswered + lastCard.delay
|
||||
def getLastCardId(fact):
|
||||
if not fact.lastCard:
|
||||
return None
|
||||
ret = [c.id for c in fact.cards if c.model.id == fact.lastCard.id]
|
||||
if ret:
|
||||
return ret[0]
|
||||
d = [{'id': f.id,
|
||||
'modelId': f.model.id,
|
||||
'created': f.created,
|
||||
'modified': f.modified,
|
||||
'tags': u",".join(f.tags),
|
||||
'spaceUntil': getSpace(f.lastCard, f.lastAnswered),
|
||||
'lastCardId': getLastCardId(f)
|
||||
} for f in oldDeck.facts]
|
||||
if d:
|
||||
s.execute(factsTable.insert(), d)
|
||||
self.total = len(oldDeck.facts)
|
||||
# fields in facts
|
||||
toAdd = []
|
||||
for oldFact in oldDeck.facts:
|
||||
for field in oldFact.model.fields:
|
||||
toAdd.append({'factId': oldFact.id,
|
||||
'id': genID(),
|
||||
'fieldModelId': fieldModels[id(field)].id,
|
||||
'ordinal': fieldModels[id(field)].ordinal,
|
||||
'value': uni(oldFact.get(field.name, u""))})
|
||||
if toAdd:
|
||||
s.execute(fieldsTable.insert(), toAdd)
|
||||
# cards
|
||||
class FakeObj(object):
|
||||
pass
|
||||
fake = FakeObj()
|
||||
fake.fact = FakeObj()
|
||||
fake.fact.model = FakeObj()
|
||||
fake.cardModel = FakeObj()
|
||||
def renderQA(c, type):
|
||||
fake.tags = u", ".join(c.tags)
|
||||
fake.fact.tags = u", ".join(c.fact.tags)
|
||||
fake.fact.model.tags = u", ".join(c.fact.model.tags)
|
||||
fake.cardModel.name = c.model.name
|
||||
return cardModels[id(c.model)].renderQA(fake, c.fact, type)
|
||||
d = [{'id': c.id,
|
||||
'created': c.created,
|
||||
'modified': c.modified,
|
||||
'factId': c.fact.id,
|
||||
'ordinal': cardModels[id(c.model)].ordinal,
|
||||
'cardModelId': cardModels[id(c.model)].id,
|
||||
'tags': u", ".join(c.tags),
|
||||
'factor': 2.5,
|
||||
'firstAnswered': c.firstAnswered,
|
||||
'interval': c.interval,
|
||||
'lastInterval': c.lastInterval,
|
||||
'modified': c.modified,
|
||||
'due': c.nextTime,
|
||||
'lastDue': c.lastTime,
|
||||
'reps': c.total,
|
||||
'question': renderQA(c, 'question'),
|
||||
'answer': renderQA(c, 'answer'),
|
||||
'averageTime': c.stats['averageTime'],
|
||||
'reviewTime': c.stats['totalTime'],
|
||||
'yesCount': (c.stats['new']['yes'] +
|
||||
c.stats['young']['yes'] +
|
||||
c.stats['old']['yes']),
|
||||
'noCount': (c.stats['new']['no'] +
|
||||
c.stats['young']['no'] +
|
||||
c.stats['old']['no']),
|
||||
'successive': c.stats['successivelyCorrect']}
|
||||
for c in oldDeck]
|
||||
if d:
|
||||
s.execute(cardsTable.insert(), d)
|
||||
# scheduler
|
||||
deck.description = uni(oldDeck.description)
|
||||
deck.created = oldDeck.created
|
||||
deck.maxScheduleTime = oldDeck.sched.maxScheduleTime
|
||||
deck.hardIntervalMin = oldDeck.sched.hardInterval[0]
|
||||
deck.hardIntervalMax = oldDeck.sched.hardInterval[1]
|
||||
deck.midIntervalMin = oldDeck.sched.midInterval[0]
|
||||
deck.midIntervalMax = oldDeck.sched.midInterval[1]
|
||||
deck.easyIntervalMin = oldDeck.sched.easyInterval[0]
|
||||
deck.easyIntervalMax = oldDeck.sched.easyInterval[1]
|
||||
deck.delay0 = oldDeck.sched.delay0
|
||||
deck.delay1 = oldDeck.sched.delay1
|
||||
deck.delay2 = oldDeck.sched.delay2
|
||||
deck.collapseTime = 3600 # oldDeck.sched.collapse
|
||||
deck.highPriority = u", ".join(oldDeck.sched.highPriority)
|
||||
deck.medPriority = u", ".join(oldDeck.sched.medPriority)
|
||||
deck.lowPriority = u", ".join(oldDeck.sched.lowPriority)
|
||||
deck.suspended = u", ".join(oldDeck.sched.suspendedTags)
|
||||
# scheduler global stats
|
||||
stats = Stats()
|
||||
stats.create(deck.s, 0, datetime.date.today())
|
||||
stats.day = datetime.date.fromtimestamp(oldDeck.created)
|
||||
stats.averageTime = oldDeck.sched.globalStats['averageTime']
|
||||
stats.reviewTime = oldDeck.sched.globalStats['totalTime']
|
||||
stats.distractedTime = 0
|
||||
stats.distractedReps = 0
|
||||
stats.newEase0 = oldDeck.sched.easeStats.get('new', {}).get(0, 0)
|
||||
stats.newEase1 = oldDeck.sched.easeStats.get('new', {}).get(1, 0)
|
||||
stats.newEase2 = oldDeck.sched.easeStats.get('new', {}).get(2, 0)
|
||||
stats.newEase3 = oldDeck.sched.easeStats.get('new', {}).get(3, 0)
|
||||
stats.newEase4 = oldDeck.sched.easeStats.get('new', {}).get(4, 0)
|
||||
stats.youngEase0 = oldDeck.sched.easeStats.get('young', {}).get(0, 0)
|
||||
stats.youngEase1 = oldDeck.sched.easeStats.get('young', {}).get(1, 0)
|
||||
stats.youngEase2 = oldDeck.sched.easeStats.get('young', {}).get(2, 0)
|
||||
stats.youngEase3 = oldDeck.sched.easeStats.get('young', {}).get(3, 0)
|
||||
stats.youngEase4 = oldDeck.sched.easeStats.get('young', {}).get(4, 0)
|
||||
stats.matureEase0 = oldDeck.sched.easeStats.get('old', {}).get(0, 0)
|
||||
stats.matureEase1 = oldDeck.sched.easeStats.get('old', {}).get(1, 0)
|
||||
stats.matureEase2 = oldDeck.sched.easeStats.get('old', {}).get(2, 0)
|
||||
stats.matureEase3 = oldDeck.sched.easeStats.get('old', {}).get(3, 0)
|
||||
stats.matureEase4 = oldDeck.sched.easeStats.get('old', {}).get(4, 0)
|
||||
yesCount = (oldDeck.sched.globalStats['new']['yes'] +
|
||||
oldDeck.sched.globalStats['young']['yes'] +
|
||||
oldDeck.sched.globalStats['old']['yes'])
|
||||
noCount = (oldDeck.sched.globalStats['new']['no'] +
|
||||
oldDeck.sched.globalStats['young']['no'] +
|
||||
oldDeck.sched.globalStats['old']['no'])
|
||||
stats.reps = yesCount + noCount
|
||||
stats.toDB(deck.s)
|
||||
# ignore daily stats & history, they make no sense on new version
|
||||
s.flush()
|
||||
deck.updateAllPriorities()
|
||||
# save without updating mod time
|
||||
deck.modified = oldDeck.modified
|
||||
deck.lastLoaded = deck.modified
|
||||
deck.s.commit()
|
||||
deck.save()
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
110
anki/models.py
110
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", "<br>")
|
||||
htmlFields[k] = '<span class="%s">%s</span>' % (k.replace(" ",""), v)
|
||||
try:
|
||||
html = field % htmlFields
|
||||
text = field % textFields
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return _("[invalid format; see model properties]")
|
||||
if not html:
|
||||
html = _("[empty]")
|
||||
text = _("[empty]")
|
||||
if format == "text":
|
||||
return text
|
||||
# add outer div & alignment (with tables due to qt's html handling)
|
||||
html = '<div class="%s">%s</div>' % (type, html)
|
||||
attr = type + 'Align'
|
||||
if getattr(self, attr) == 0:
|
||||
align = "center"
|
||||
elif getattr(self, attr) == 1:
|
||||
align = "left"
|
||||
else:
|
||||
align = "right"
|
||||
html = (("<center><table width=95%%><tr><td align=%s>" % align) +
|
||||
html + "</td></tr></table></center>")
|
||||
return html
|
||||
|
||||
def renderQASQL(self, type, factId):
|
||||
"Render QA in pure SQL, with no HTML generation."
|
||||
fields = dict(object_session(self).all("""
|
||||
select fieldModels.name, fields.value from fields, fieldModels
|
||||
where
|
||||
fields.factId = :factId and
|
||||
fields.fieldModelId = fieldModels.id""", factId=factId))
|
||||
fields['tags'] = u""
|
||||
for (k, v) in fields.items():
|
||||
fields["text:"+k] = v
|
||||
if type == "q": format = self.qformat
|
||||
else: format = self.aformat
|
||||
try:
|
||||
return format % fields
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return _("[empty]")
|
||||
|
||||
def css(self):
|
||||
"Return the CSS markup for this card."
|
||||
t = ""
|
||||
for type in ("question", "answer"):
|
||||
t += ".%s { font-family: \"%s\"; color: %s; font-size: %dpx; }\n" % (
|
||||
type,
|
||||
toPlatformFont(getattr(self, type+"FontFamily")),
|
||||
getattr(self, type+"FontColour"),
|
||||
getattr(self, type+"FontSize"))
|
||||
return t
|
||||
|
||||
mapper(CardModel, cardModelsTable)
|
||||
|
||||
def formatQA(cid, mid, fact, tags, cm):
|
||||
"Return a dict of {id, question, answer}"
|
||||
d = {'id': cid}
|
||||
fields = {}
|
||||
for (k, v) in fact.items():
|
||||
fields["text:"+k] = v[1]
|
||||
fields[k] = '<span class="fm%s">%s</span>' % (
|
||||
hexifyID(v[0]), v[1])
|
||||
fields['tags'] = canonifyTags(tags)
|
||||
# render q & a
|
||||
ret = []
|
||||
for (type, format) in (("question", cm.qformat),
|
||||
("answer", cm.aformat)):
|
||||
try:
|
||||
html = format % fields
|
||||
except (KeyError, TypeError, ValueError):
|
||||
html = _("[invalid question/answer format]")
|
||||
d[type] = html
|
||||
return d
|
||||
|
||||
# Model table
|
||||
##########################################################################
|
||||
|
||||
|
|
|
|||
|
|
@ -323,8 +323,8 @@ class DeckStats(object):
|
|||
d = self.deck
|
||||
html="<h1>" + _("Deck Statistics") + "</h1>"
|
||||
html += _("Deck created: <b>%s</b> ago<br>") % self.createdTimeStr()
|
||||
total = d.cardCount()
|
||||
new = d.newCardCount()
|
||||
total = d.cardCount
|
||||
new = d.newCount
|
||||
young = d.youngCardCount()
|
||||
old = d.matureCardCount()
|
||||
newP = new / float(total) * 100
|
||||
|
|
@ -352,7 +352,7 @@ class DeckStats(object):
|
|||
html += _("First-seen cards: <b>%(gNewYes%)0.1f%%</b> "
|
||||
"(<b>%(gNewYes)d</b> of <b>%(gNewTotal)d</b>)<br><br>") % stats
|
||||
# average pending time
|
||||
existing = d.cardCount() - d.newCardCount()
|
||||
existing = d.cardCount - d.newTodayCount
|
||||
avgInt = self.getAverageInterval()
|
||||
def tr(a, b):
|
||||
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
|
||||
|
|
@ -418,7 +418,7 @@ class DeckStats(object):
|
|||
|
||||
def newAverage(self):
|
||||
"Average number of new cards added each day."
|
||||
return self.deck.cardCount() / max(1, self.ageInDays())
|
||||
return self.deck.cardCount / max(1, self.ageInDays())
|
||||
|
||||
def createdTimeStr(self):
|
||||
return anki.utils.fmtTimeSpan(time.time() - self.deck.created)
|
||||
|
|
|
|||
27
anki/sync.py
27
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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue