diff -Nurx '*.py~' anki-1.2.8.orig/anki anki-1.2.8/anki
--- anki-1.2.8.orig/anki 2011-03-28 15:33:31.000000000 +0800
+++ anki-1.2.8/anki 2012-03-28 15:55:56.729152701 +0800
@@ -24,4 +24,12 @@
except ImportError:
raise Exception("You need to run tools/build_ui.sh in order for anki to work.")
- ankiqt.run()
+ import cProfile
+ for i in xrange(30):
+ proFile = '/home/siemer/.anki/anki.profile.' + str(i)
+ if not os.path.exists(proFile):
+ break
+ if True:
+ cProfile.run('ankiqt.run()', proFile)
+ else:
+ ankiqt.run()
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/forms/main.py anki-1.2.8/ankiqt/forms/main.py
--- anki-1.2.8.orig/ankiqt/forms/main.py 2011-03-28 15:37:05.000000000 +0800
+++ anki-1.2.8/ankiqt/forms/main.py 2012-03-29 14:23:15.654011313 +0800
@@ -1548,9 +1548,9 @@
self.actionMarkCard.setShortcut(_("Ctrl+M"))
self.actionSuspendCard.setText(_("&Suspend Card"))
self.actionSuspendCard.setStatusTip(_("Stop reviewing this card until it\'s unsuspended in the browser"))
- self.actionSuspendCard.setShortcut(_("Ctrl+Shift+S"))
+ self.actionSuspendCard.setShortcut(_("0"))
self.actionRepeatAudio.setText(_("Repeat &Audio"))
- self.actionRepeatAudio.setShortcut(_("F5"))
+ self.actionRepeatAudio.setShortcut(_("F3"))
self.actionUndo.setText(_("&Undo"))
self.actionUndo.setShortcut(_("Ctrl+Z"))
self.actionForum.setText(_("&Forum..."))
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/ui/addcards.py anki-1.2.8/ankiqt/ui/addcards.py
--- anki-1.2.8.orig/ankiqt/ui/addcards.py 2011-03-28 15:33:31.000000000 +0800
+++ anki-1.2.8/ankiqt/ui/addcards.py 2012-04-01 14:37:21.377972583 +0800
@@ -117,8 +117,6 @@
break
n += 1
fact.tags = oldFact.tags
- else:
- fact.tags = self.parent.deck.lastTags
# set the new fact
self.editor.setFact(fact, check=True, forceRedraw=True)
self.setTabOrder(self.editor.tags, self.addButton)
@@ -153,18 +151,6 @@
# we don't reset() until the add cards dialog is closed
return fact
- def initializeNewFact(self, old_fact):
- f = self.parent.deck.newFact()
- f.tags = self.parent.deck.lastTags
- return f
-
- def clearOldFact(self, old_fact):
- f = self.initializeNewFact(old_fact)
- self.editor.setFact(f, check=True, scroll=True)
- # let completer know our extra tags
- self.editor.tags.addTags(parseTags(self.parent.deck.lastTags))
- return f
-
def addCards(self):
# make sure updated
self.editor.saveFieldsNow()
@@ -185,7 +171,7 @@
self.parent.statusView.redraw()
# start a new fact
- self.clearOldFact(fact)
+ self.editor.setFact(self.parent.deck.newFact(), check=True, scroll=True)
self.maybeSave()
@@ -219,7 +205,6 @@
self.editor.close()
ui.dialogs.close("AddCards")
self.parent.deck.s.flush()
- self.parent.deck.rebuildCSS()
self.parent.reset()
saveGeom(self, "add")
saveSplitter(self.dialog.splitter, "add")
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/ui/cardlist.py anki-1.2.8/ankiqt/ui/cardlist.py
--- anki-1.2.8.orig/ankiqt/ui/cardlist.py 2011-03-28 15:33:31.000000000 +0800
+++ anki-1.2.8/ankiqt/ui/cardlist.py 2012-03-29 15:12:34.908685446 +0800
@@ -537,7 +537,6 @@
self.sortKey = "firstAnswered"
else:
self.sortKey = ("field", self.sortFields[idx-11])
- self.rebuildSortIndex(self.sortKey)
self.sortIndex = idx
self.deck.setVar('sortIndex', idx)
self.model.sortKey = self.sortKey
@@ -548,28 +547,6 @@
self.onEvent()
self.focusCurrentCard()
- def rebuildSortIndex(self, key):
- if key not in (
- "question", "answer", "created", "modified", "due", "interval",
- "reps", "factor", "noCount", "firstAnswered"):
- return
- old = self.deck.s.scalar("select sql from sqlite_master where name = :k",
- k="ix_cards_sort")
- if old and key in old:
- return
- self.parent.setProgressParent(self)
- self.deck.startProgress(2)
- self.deck.updateProgress(_("Building Index..."))
- self.deck.s.statement("drop index if exists ix_cards_sort")
- self.deck.updateProgress()
- if key in ("question", "answer"):
- key = key + " collate nocase"
- self.deck.s.statement(
- "create index ix_cards_sort on cards (%s)" % key)
- self.deck.s.statement("analyze")
- self.deck.finishProgress()
- self.parent.setProgressParent(None)
-
def tagChanged(self, idx):
if idx == 0:
filter = ""
@@ -938,7 +915,6 @@
return
self.deck.rescheduleCards(self.selectedCards(), min, max)
finally:
- self.deck.reset()
self.deck.setUndoEnd(n)
self.updateAfterCardChange()
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/ui/clayout.py anki-1.2.8/ankiqt/ui/clayout.py
--- anki-1.2.8.orig/ankiqt/ui/clayout.py 2011-03-28 15:33:31.000000000 +0800
+++ anki-1.2.8/ankiqt/ui/clayout.py 2012-04-01 14:38:02.934178646 +0800
@@ -180,15 +180,6 @@
self.card.cardModel.aformat = text
self.fact.model.setModified()
self.deck.flushMod()
- d = {}
- for f in self.fact.model.fieldModels:
- d[f.name] = (f.id, self.fact[f.name])
- for card in self.cards:
- qa = formatQA(None, self.fact.modelId, d, card.splitTags(),
- card.cardModel, self.deck)
- card.question = qa['question']
- card.answer = qa['answer']
- card.setModified()
self.deck.setModified()
self.needFormatRebuild = True
self.renderPreview()
@@ -279,22 +270,13 @@
def renderPreview(self):
c = self.card
- styles = (self.deck.rebuildCSS() +
- ("\nhtml { background: %s }" % c.cardModel.lastFontColour))
- styles = runFilter("addStyles", styles, c)
self.form.preview.setHtml(
- ('
%s' % getBase(self.deck, c)) +
- "" +
- runFilter("drawQuestion", mungeQA(self.deck, c.htmlQuestion()),
- c) +
- "
" +
- runFilter("drawAnswer", mungeQA(self.deck, c.htmlAnswer()),
- c)
- + "")
+ runFilter("drawQuestion", stripSounds(c.getQuestion()), c) + "
" +
+ runFilter("drawAnswer", stripSounds(c.getAnswer()), c),
+ self.deck.mediaQUrl())
clearAudioQueue()
if c.id not in self.playedAudio:
- playFromText(c.question)
- playFromText(c.answer)
+ playFromText(c.getQuestion() + c.getAnswer())
self.playedAudio[c.id] = True
def reject(self):
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/ui/main.py anki-1.2.8/ankiqt/ui/main.py
--- anki-1.2.8.orig/ankiqt/ui/main.py 2011-03-28 15:33:31.000000000 +0800
+++ anki-1.2.8/ankiqt/ui/main.py 2012-04-01 23:20:29.469617717 +0800
@@ -1,5 +1,6 @@
-# Copyright: Damien Elmes
# -*- coding: utf-8 -*-
+# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import os, sys, re, types, gettext, stat, traceback, inspect, signal
@@ -15,7 +16,7 @@
from anki.errors import *
from anki.sound import hasSound, playFromText, clearAudioQueue, stripSounds
from anki.utils import addTags, deleteTags, parseTags, canonifyTags, \
- stripHTML, checksum
+ stripHTML, checksum, fmtTimeSpan
from anki.media import rebuildMediaDir, downloadMissing, downloadRemote
from anki.db import OperationalError, SessionHelper, sqlite
from anki.stdmodels import BasicModel
@@ -47,6 +48,7 @@
self.deck = None
self.state = "initial"
self.hideWelcome = False
+ self.nextCard = None
self.views = []
signal.signal(signal.SIGINT, self.onSigInt)
self.setLang()
@@ -77,7 +79,7 @@
self.resize(500, 500)
# load deck
ui.splash.update()
- self.setupErrorHandler()
+ #self.setupErrorHandler()
self.setupMisc()
# check if we've been updated
if "version" not in self.config:
@@ -131,7 +133,7 @@
self.close()
def onReload(self):
- self.moveToState("auto")
+ self.moveToState("reload")
def setupMainWindow(self):
# main window
@@ -146,12 +148,12 @@
SIGNAL("triggered()"),
self.onReload)
# congrats
- self.connect(self.mainWin.learnMoreButton,
- SIGNAL("clicked()"),
- self.onLearnMore)
- self.connect(self.mainWin.reviewEarlyButton,
- SIGNAL("clicked()"),
- self.onReviewEarly)
+ #self.connect(self.mainWin.learnMoreButton,
+ # SIGNAL("clicked()"),
+ # self.onLearnMore)
+ #self.connect(self.mainWin.reviewEarlyButton,
+ # SIGNAL("clicked()"),
+ # self.onReviewEarly)
self.connect(self.mainWin.finishButton,
SIGNAL("clicked()"),
self.onClose)
@@ -164,7 +166,6 @@
elif sys.platform.startswith("darwin"):
self.mainWin.noticeButton.setFixedWidth(20)
self.mainWin.noticeButton.setFixedHeight(20)
- addHook("cardAnswered", self.onCardAnsweredHook)
addHook("undoEnd", self.maybeEnableUndo)
addHook("notify", self.onNotify)
@@ -175,13 +176,6 @@
else:
ui.utils.showInfo(msg)
- def setNotice(self, str=""):
- if str:
- self.mainWin.noticeLabel.setText(str)
- self.mainWin.noticeFrame.setShown(True)
- else:
- self.mainWin.noticeFrame.setShown(False)
-
def setupViews(self):
self.bodyView = ui.view.View(self, self.mainWin.mainText,
self.mainWin.mainTextFrame)
@@ -307,35 +301,19 @@
self.views = self.viewsBackup
self.viewsBackup = None
- def reset(self, count=True, priorities=False, runHooks=True):
+ def reset(self):
if self.deck:
self.deck.refreshSession()
- if priorities:
- self.deck.updateAllPriorities()
- self.deck.reset()
- if runHooks:
- runHook("guiReset")
+ runHook("guiReset")
self.moveToState("initial")
def moveToState(self, state):
+ print state
t = time.time()
- if state == "initial":
- # reset current card and load again
- self.currentCard = None
- self.lastCard = None
- self.editor.deck = self.deck
- if self.deck:
+ if state == "initial" and self.deck:
self.enableDeckMenuItems()
self.updateViews(state)
- if self.state == "studyScreen":
- return self.moveToState("studyScreen")
- else:
- return self.moveToState("getQuestion")
- else:
- return self.moveToState("noDeck")
- elif state == "auto":
- self.currentCard = None
- self.lastCard = None
+ if state in ("initial", "reload"):
if self.deck:
if self.state == "studyScreen":
return self.moveToState("studyScreen")
@@ -346,14 +324,12 @@
# save the new & last state
self.lastState = getattr(self, "state", None)
self.state = state
- self.updateTitleBar()
if 'state' != 'noDeck' and state != 'editCurrentFact':
self.switchToReviewScreen()
- if state == "noDeck":
+ if state == "noDeck": # means: drop the deck unconditionally, init vars
self.deck = None
self.help.hide()
- self.currentCard = None
- self.lastCard = None
+ self.currentCard = self.lastCard = self.nextCard = None
self.disableDeckMenuItems()
# hide all deck-associated dialogs
self.closeAllDeckWindows()
@@ -364,27 +340,14 @@
if self.deck.isEmpty():
return self.moveToState("deckEmpty")
else:
- # timeboxing only supported using the standard scheduler
- if not self.deck.finishScheduler:
- if (self.config['showStudyScreen'] and
- not self.deck.sessionStartTime):
- return self.moveToState("studyScreen")
- if self.deck.sessionLimitReached():
- self.showToolTip(_("Session limit reached."))
- self.moveToState("studyScreen")
- # switch to timeboxing screen
- self.mainWin.tabWidget.setCurrentIndex(2)
- return
if not self.currentCard:
- self.currentCard = self.deck.getCard()
+ if not self.nextCard:
+ self.currentCard = self.deck.getCard()
+ else:
+ self.currentCard, self.nextCard = self.nextCard, None
+ QTimer.singleShot(0, self.getNextCard)
if self.currentCard:
- if self.lastCard:
- if self.lastCard.id == self.currentCard.id:
- pass
- # if self.currentCard.combinedDue > time.time():
- # # if the same card is being shown and it's not
- # # due yet, give up
- # return self.moveToState("deckFinished")
+ self.currentCard.startTimer()
self.enableCardMenuItems()
return self.moveToState("showQuestion")
else:
@@ -399,8 +362,7 @@
self.disableCardMenuItems()
self.switchToCongratsScreen()
self.mainWin.learnMoreButton.setEnabled(
- not not self.deck.newCount)
- self.startRefreshTimer()
+ not not self.deck.newCount())
self.bodyView.setState(state)
# focus finish button
self.mainWin.finishButton.setFocus()
@@ -413,14 +375,19 @@
self.updateMarkAction()
runHook('showQuestion')
elif state == "showAnswer":
+ print 'showA: %f' % time.time()
self.showEaseButtons()
self.enableCardMenuItems()
elif state == "editCurrentFact":
if self.lastState == "editCurrentFact":
return self.moveToState("saveEdit")
self.mainWin.actionRepeatAudio.setEnabled(False)
+ self.mainWin.buttonStack.hide()
self.deck.s.flush()
- self.showEditor()
+ self.editor.deck = self.deck
+ self.switchToEditScreen()
+ self.editor.setFact(self.currentCard.fact)
+ self.editor.card = self.currentCard
elif state == "saveEdit":
self.mainWin.actionRepeatAudio.setEnabled(True)
self.editor.saveFieldsNow()
@@ -428,10 +395,9 @@
return self.reset()
elif state == "studyScreen":
self.currentCard = None
- if self.deck.finishScheduler:
- self.deck.finishScheduler()
self.disableCardMenuItems()
self.showStudyScreen()
+ self.updateTitleBar()
self.updateViews(state)
def keyPressEvent(self, evt):
@@ -490,84 +456,20 @@
def cardAnswered(self, quality):
"Reschedule current card and move back to getQuestion state."
+ print 'cardA: %f' % time.time()
if self.state != "showAnswer":
return
- # force refresh of card then remove from session as we update in pure sql
- self.deck.s.refresh(self.currentCard)
- self.deck.s.refresh(self.currentCard.fact)
- self.deck.s.refresh(self.currentCard.cardModel)
- self.deck.s.expunge(self.currentCard)
- # answer
- self.deck.answerCard(self.currentCard, quality)
+ QTimer.singleShot(0, lambda card=self.currentCard, time=self.currentCard.thinkingTime():
+ self.deck.answerCard(card, quality, time))
self.lastQuality = quality
self.lastCard = self.currentCard
self.currentCard = None
- if self.config['saveAfterAnswer']:
- num = self.config['saveAfterAnswerNum']
- stats = self.deck.getStats()
- if stats['gTotal'] % num == 0:
- self.save()
self.moveToState("getQuestion")
+ QTimer.singleShot(0, self.save)
- def onCardAnsweredHook(self, cardId, isLeech):
- if not isLeech:
- self.setNotice()
- return
- txt = (_("""\
-%s... is a leech.""")
- % stripHTML(stripSounds(self.currentCard.question)).\
- replace("\n", " ")[0:30])
- if isLeech and self.deck.s.scalar(
- "select 1 from cards where id = :id and priority < 1", id=cardId):
- txt += _(" It has been suspended.")
- self.setNotice(txt)
-
- def startRefreshTimer(self):
- "Update the screen once a minute until next card is displayed."
- if getattr(self, 'refreshTimer', None):
- return
- self.refreshTimer = QTimer(self)
- self.refreshTimer.start(60000)
- self.connect(self.refreshTimer, SIGNAL("timeout()"), self.refreshStatus)
- # start another time to refresh exactly after we've finished
- next = self.deck.earliestTime()
- if next:
- delay = next - time.time()
- if delay > 86400:
- return
- if delay < 0:
- c = self.deck.getCard()
- if c:
- return self.moveToState("auto")
- sys.stderr.write("""\
-earliest time returned %f
-
-please report this error, but it's not serious.
-closing and opening your deck should fix it.
-
-counts are %d %d %d
-""" % (delay,
- self.deck.failedSoonCount,
- self.deck.revCount,
- self.deck.newCountToday))
- return
- t = QTimer(self)
- t.setSingleShot(True)
- self.connect(t, SIGNAL("timeout()"), self.refreshStatus)
- t.start((delay+1)*1000)
-
- def refreshStatus(self):
- "If triggered when the deck is finished, reset state."
- if self.inDbHandler:
- return
- if self.state == "deckFinished":
- # don't try refresh if the deck is closed during a sync
- if self.deck:
- self.moveToState("getQuestion")
- if self.state != "deckFinished":
- if self.refreshTimer:
- self.refreshTimer.stop()
- self.refreshTimer = None
+ def getNextCard(self):
+ self.nextCard = self.deck.getCard()
+ self.nextCard.fact.fieldsDict # trigger lazy loading of fields
# Main stack
##########################################################################
@@ -615,8 +517,7 @@
for i in range(1, 5):
b = getattr(self.mainWin, "easeButton%d" % i)
b.setFixedWidth(85)
- self.connect(b, SIGNAL("clicked()"),
- lambda i=i: self.cardAnswered(i))
+ self.connect(b, SIGNAL("clicked()"), lambda i=i: self.cardAnswered(i))
# type answer
outer = QHBoxLayout()
outer.setSpacing(0)
@@ -701,8 +602,7 @@
txt = '%s' % txt
l.setText(txt)
else:
- txt = self.deck.nextIntervalStr(
- self.currentCard, i)
+ txt = fmtTimeSpan(self.deck.nextInterval(self.currentCard, i)*86400)
txt = "" + txt + ""
if i == self.defaultEaseButton() and self.config['colourTimes']:
txt = '' + txt + ''
@@ -765,7 +665,6 @@
except:
ui.utils.showWarning(
_("Unable to recover. Deck load failed."))
- self.deck = None
else:
self.deck = None
return 0
@@ -855,17 +754,15 @@
self.closeAllDeckWindows()
synced = False
if self.deck is not None:
- if self.deck.finishScheduler:
- self.deck.finishScheduler()
- self.deck.reset()
# update counts
for d in self.browserDecks:
if d['path'] == self.deck.path:
- d['due'] = self.deck.failedSoonCount + self.deck.revCount
- d['new'] = self.deck.newCountToday
+ d['due'] = self.deck.revCount()
+ d['new'] = self.deck.newCount()
d['mod'] = self.deck.modified
- d['time'] = self.deck._dailyStats.reviewTime
- d['reps'] = self.deck._dailyStats.reps
+ dailyStats = anki.stats.raiseStats(self.deck.s, today=True)
+ d['time'] = dailyStats.reviewTime
+ d['reps'] = dailyStats.reps
if self.deck.modifiedSinceSave():
if (self.deck.path is None or
(not self.config['saveOnClose'] and
@@ -1188,14 +1085,15 @@
try:
mod = os.stat(d)[stat.ST_MTIME]
deck = DeckStorage.Deck(d, backup=False)
+ dailyStats = anki.stats.raiseStats(deck.s, today=True)
self.browserDecks.append({
'path': d,
'name': deck.name(),
- 'due': deck.failedSoonCount + deck.revCount,
- 'new': deck.newCountToday,
+ 'due': deck.revCount(),
+ 'new': deck.newCount(),
'mod': deck.modified,
- 'time': deck._dailyStats.reviewTime,
- 'reps': deck._dailyStats.reps,
+ 'time': dailyStats.reviewTime,
+ 'reps': dailyStats.reps,
})
deck.close()
try:
@@ -1207,6 +1105,7 @@
if "File is in use" in unicode(e):
continue
else:
+ traceback.print_exc()
toRemove.append(d)
for d in toRemove:
self.config['recentDeckPaths'].remove(d)
@@ -1279,8 +1178,7 @@
lim = self.config['deckBrowserNameLength']
if len(n) > lim:
n = n[:lim] + "..."
- mod = _("%s ago") % anki.utils.fmtTimeSpan(
- time.time() - deck['mod'])
+ mod = _("%s ago") % fmtTimeSpan(time.time() - deck['mod'])
mod = "%s" % mod
l = QLabel("%d. %s
%s" %
(c+1, n, mod))
@@ -1375,7 +1273,7 @@
"Studied %(reps)d cards in %(time)s today.",
reps) % {
'reps': reps,
- 'time': anki.utils.fmtTimeSpan(mins, point=2),
+ 'time': fmtTimeSpan(mins, point=2),
}
rev = ngettext(
"%d review",
@@ -1408,7 +1306,7 @@
self.browserDecks[c]['name']):
self.config['recentDeckPaths'].remove(self.browserDecks[c]['path'])
del self.browserDecks[c]
- self.doLater(100, self.showDeckBrowser)
+ QTimer.singleShot(100, self.showDeckBrowser)
def onDeckBrowserDelete(self, c):
deck = self.browserDecks[c]['path']
@@ -1423,17 +1321,11 @@
except OSError:
pass
self.config['recentDeckPaths'].remove(deck)
- self.doLater(100, self.showDeckBrowser)
+ QTimer.singleShot(100, self.showDeckBrowser)
def onDeckBrowserForgetInaccessible(self):
self.refreshBrowserDecks(forget=True)
- def doLater(self, msecs, func):
- timer = QTimer(self)
- timer.setSingleShot(True)
- timer.start(msecs)
- self.connect(timer, SIGNAL("timeout()"), func)
-
# Opening and closing the app
##########################################################################
@@ -1518,13 +1410,6 @@
self.connect(self.mainWin.saveEditorButton, SIGNAL("clicked()"),
lambda: self.moveToState("saveEdit"))
-
- def showEditor(self):
- self.mainWin.buttonStack.hide()
- self.switchToEditScreen()
- self.editor.setFact(self.currentCard.fact)
- self.editor.card = self.currentCard
-
def onFactValid(self, fact):
self.mainWin.saveEditorButton.setEnabled(True)
@@ -1614,7 +1499,6 @@
except ValueError:
pass
self.deck.flushMod()
- self.deck.reset()
self.statusView.redraw()
self.updateStudyStats()
@@ -1657,9 +1541,7 @@
def updateStudyStats(self):
self.mainWin.buttonStack.hide()
- self.deck.reset()
self.updateActives()
- wasReached = self.deck.sessionLimitReached()
sessionColour = '%s'
cardColour = '%s'
# top label
@@ -1667,9 +1549,9 @@
s = self.deck.getStats()
h['ret'] = cardColour % (s['rev']+s['failed'])
h['new'] = cardColour % s['new']
- h['newof'] = str(self.deck.newCountAll())
+ h['newof'] = str(self.deck.newCount())
dtoday = s['dTotal']
- yesterday = self.deck._dailyStats.day - datetime.timedelta(1)
+ yesterday = anki.stats.raiseStats(self.deck.s, today=True).day - datetime.timedelta(1)
res = self.deck.s.first("""
select reps, reviewTime from stats where type = 1 and
day = :d""", d=yesterday)
@@ -1679,23 +1561,11 @@
dyest = 0; tyest = 0
h['repsToday'] = sessionColour % dtoday
h['repsTodayChg'] = str(dyest)
- limit = self.deck.sessionTimeLimit
- start = self.deck.sessionStartTime or time.time() - limit
- start2 = self.deck.lastSessionStart or start - limit
- last10 = self.deck.s.scalar(
- "select count(*) from reviewHistory where time >= :t",
- t=start)
- last20 = self.deck.s.scalar(
- "select count(*) from reviewHistory where "
- "time >= :t and time < :t2",
- t=start2, t2=start)
- h['repsInSes'] = sessionColour % last10
- h['repsInSesChg'] = str(last20)
+ h['repsInSes'] = sessionColour % 'unknown'
+ h['repsInSesChg'] = 'unknown'
ttoday = s['dReviewTime']
- h['timeToday'] = sessionColour % (
- anki.utils.fmtTimeSpan(ttoday, short=True, point=1))
- h['timeTodayChg'] = str(anki.utils.fmtTimeSpan(
- tyest, short=True, point=1))
+ h['timeToday'] = sessionColour % (fmtTimeSpan(ttoday, short=True, point=1))
+ h['timeTodayChg'] = fmtTimeSpan(tyest, short=True, point=1)
h['cs_header'] = "" + _("Cards/session:") + ""
h['cd_header'] = "" + _("Cards/day:") + ""
h['td_header'] = "" + _("Time/day:") + ""
@@ -1802,16 +1672,11 @@
self.deck.setFailedCardPolicy(
self.mainWin.failedCardsOption.currentIndex())
self.deck.flushMod()
- self.deck.reset()
- if not self.deck.finishScheduler:
- self.deck.startSession()
self.config['studyOptionsScreen'] = self.mainWin.tabWidget.currentIndex()
self.moveToState("getQuestion")
def onStudyOptions(self):
- if self.state == "studyScreen":
- pass
- else:
+ if self.state != "studyScreen":
self.moveToState("studyScreen")
# Toolbar
@@ -1899,14 +1764,14 @@
self.currentCard.fact.tags = canonifyTags(addTags(
"Marked", self.currentCard.fact.tags))
self.currentCard.fact.setModified(textChanged=True, deck=self.deck)
- self.deck.updateFactTags([self.currentCard.fact.id])
self.deck.setModified()
def onSuspend(self):
undo = _("Suspend")
self.deck.setUndoStart(undo)
- self.deck.suspendCards([self.currentCard.id])
- self.reset()
+ cardId = self.currentCard.id
+ self.cardAnswered(4)
+ self.deck.suspendCards([cardId])
self.deck.setUndoEnd(undo)
def onDelete(self):
@@ -2046,8 +1911,6 @@
if self.state == "studyScreen":
self.onStartReview()
else:
- self.deck.reset()
- self.deck.getCard() # so scheduler will reset if empty
self.moveToState("initial")
if not self.deck.finishScheduler:
ui.utils.showInfo(_("No cards matched the provided tags."))
@@ -2091,14 +1954,13 @@
self.deck.updateProgress()
d.s.statement("vacuum")
self.deck.updateProgress()
- nfacts = d.factCount
mdir = self.deck.mediaDir()
d.close()
dir = os.path.dirname(path)
zippath = os.path.join(dir, "shared-%d.zip" % time.time())
# zip it up
zip = zipfile.ZipFile(zippath, "w", zipfile.ZIP_DEFLATED)
- zip.writestr("facts", str(nfacts))
+ zip.writestr("facts", str(d.factCount()))
zip.writestr("version", str(2))
readmep = os.path.join(dir, "README.html")
readme = open(readmep, "w")
@@ -2186,7 +2048,6 @@
def syncDeck(self, interactive=True, onlyMerge=False, reload=True):
"Synchronise a deck with the server."
if not self.inMainWindow() and interactive and interactive!=-1: return
- self.setNotice()
# vet input
if interactive:
self.ensureSyncParams()
@@ -2379,7 +2240,6 @@
def cleanNewDeck(self):
"Unload a new deck if an initial sync failed."
- self.deck = None
self.deckPath = None
self.moveToState("noDeck")
self.syncFinished = True
@@ -2511,7 +2371,7 @@
self.connect(m.actionCheckMediaDatabase, s, self.onCheckMediaDB)
self.connect(m.actionDownloadMissingMedia, s, self.onDownloadMissingMedia)
self.connect(m.actionLocalizeMedia, s, self.onLocalizeMedia)
- self.connect(m.actionCram, s, self.onCram)
+ # self.connect(m.actionCram, s, self.onCram)
self.connect(m.actionOpenPluginFolder, s, self.onOpenPluginFolder)
self.connect(m.actionEnableAllPlugins, s, self.onEnableAllPlugins)
self.connect(m.actionDisableAllPlugins, s, self.onDisableAllPlugins)
@@ -2542,15 +2402,15 @@
deckpath = self.deck.name()
if self.deck.modifiedSinceSave():
deckpath += "*"
- if not self.config['showProgress']:
+ if not self.config['showProgress'] or True:
title = deckpath + " - " + title
else:
title = _("%(path)s (%(due)d of %(cards)d due)"
" - %(title)s") % {
"path": deckpath,
"title": title,
- "cards": self.deck.cardCount,
- "due": self.deck.failedSoonCount + self.deck.revCount
+ "cards": self.deck.cardCount(),
+ "due": self.deck.revCount()
}
self.setWindowTitle(title)
@@ -2581,19 +2441,17 @@
def enableCardMenuItems(self):
self.maybeEnableUndo()
- snd = (hasSound(self.currentCard.question) or
- (hasSound(self.currentCard.answer) and
- self.state != "getQuestion"))
- self.mainWin.actionEditLayout.setEnabled(True)
- self.mainWin.actionRepeatAudio.setEnabled(snd)
- self.mainWin.actionMarkCard.setEnabled(True)
- self.mainWin.actionSuspendCard.setEnabled(True)
- self.mainWin.actionDelete.setEnabled(True)
- self.mainWin.actionBuryFact.setEnabled(True)
+ self.mainWin.actionRepeatAudio.setEnabled(True)
+ self.mainWin.actionMarkCard.setEnabled(True)
+ self.mainWin.actionSuspendCard.setEnabled(True)
+ self.mainWin.actionDelete.setEnabled(True)
+ self.mainWin.actionBuryFact.setEnabled(True)
+ # print 'preventEditUntilAnswer', self.config['preventEditUntilAnswer']
enableEdits = (not self.config['preventEditUntilAnswer'] or
- self.state != "getQuestion")
+ self.state != "getQuestion")
self.mainWin.actionEditCurrent.setEnabled(enableEdits)
self.mainWin.actionEditdeck.setEnabled(enableEdits)
+ self.mainWin.actionEditLayout.setEnabled(enableEdits)
runHook("enableCardMenuItems")
def maybeEnableUndo(self):
@@ -2838,12 +2696,14 @@
def onRepeatAudio(self):
clearAudioQueue()
+ audio = u''
if (not self.currentCard.cardModel.questionInAnswer
or self.state == "showQuestion") and \
self.config['repeatQuestionAudio']:
- playFromText(self.currentCard.question)
+ audio += self.currentCard.getQuestion()
if self.state != "showQuestion":
- playFromText(self.currentCard.answer)
+ audio += self.currentCard.getAnswer()
+ playFromText(audio)
def onRecordNoiseProfile(self):
from ankiqt.ui.sound import recordNoiseProfile
@@ -2863,7 +2723,6 @@
self.busyCursor = False
self.updatingBusy = False
self.mainThread = QThread.currentThread()
- self.oldSessionHelperGetter = SessionHelper.__getattr__
SessionHelper.__getattr__ = wrap(SessionHelper.__getattr__,
self.checkProgressHandler,
pos="before")
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/ui/status.py anki-1.2.8/ankiqt/ui/status.py
--- anki-1.2.8.orig/ankiqt/ui/status.py 2011-03-28 15:33:31.000000000 +0800
+++ anki-1.2.8/ankiqt/ui/status.py 2012-04-01 02:02:19.831734522 +0800
@@ -1,4 +1,5 @@
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
from PyQt4.QtGui import *
@@ -7,6 +8,7 @@
import sys, time
from ankiqt import ui
from anki.hooks import addHook
+from collections import namedtuple
class QClickableLabel(QLabel):
url = "http://ichi2.net/anki/wiki/TheTimerAndShortQuestions"
@@ -159,55 +161,33 @@
def redraw(self):
p = QPalette()
stats = self.main.deck.getStats()
- remStr = _("Remaining: ")
- if self.state == "deckFinished":
- remStr += "0"
- elif self.state == "deckEmpty":
- remStr += "0"
+ if self.main.currentCard:
+ card = self.main.currentCard
else:
- # remaining string, bolded depending on current card
- if sys.platform.startswith("linux"):
- s = " "
- else:
- s = " "
- if not self.main.currentCard:
- remStr += "%(failed1)s" + s + "%(rev1)s" + s + "%(new1)s"
- else:
- t = self.main.deck.cardQueue(self.main.currentCard)
- if t == 0:
- remStr += ("%(failed1)s" + s +
- "%(rev1)s" + s + "%(new1)s")
- elif t == 1:
- remStr += ("%(failed1)s" + s + "%(rev1)s" + s +
- "%(new1)s")
- else:
- remStr += ("%(failed1)s" + s + "%(rev1)s" + s +
- "%(new1)s")
- stats['failed1'] = '%s' % stats['failed']
- stats['rev1'] = '%s' % stats['rev']
- new = stats['new']
- stats['new1'] = '%s' % new
- self.remText.setText(remStr % stats)
- stats['spaced'] = self.main.deck.spacedCardCount()
- stats['new2'] = self.main.deck.newCount
- self.remText.setToolTip("" +_(
- "Remaining cards") + "
" +
- ngettext("There is %d failed card due soon.", \
- "There are %d failed cards due soon.", \
- stats['failed']) % stats['failed'] + "
" +
- ngettext("There is %d card awaiting review.",
- "There are %d cards awaiting review.", \
- stats['rev']) % stats['rev'] + "
" +
- ngettext("There is %d new card due today.", \
- "There are %d new cards due today.",\
- stats['new']) % stats['new'] + "
" +
- ngettext("There is %d new card in total.", \
- "There are %d new cards in total.",\
- stats['new2']) % stats['new2'] + "
" +
- ngettext("There is %d delayed card.", \
- "There are %d delayed cards.", \
- stats['spaced']) % stats['spaced'])
- # eta
+ card = namedtuple('FakeCard', 'isNew, isReview, isFailed')(False, False, False)
+ underline = (('', ''), ('', ''))
+ self.remText.setText(_("Remaining: "
+ "{0[0]}{failed}{0[1]} "
+ "{1[0]}{rev}{1[1]} "
+ "{2[0]}{new}{2[1]}").format(
+ underline[card.isFailed],
+ underline[card.isReview],
+ underline[card.isNew], **stats))
+ stats['newTotal'] = self.main.deck.newCount()
+ sentences = ((
+ "There is {} failed card due soon.",
+ "There are {} failed cards due soon.", "failed"), (
+ "There is {} card awaiting review.",
+ "There are {} cards awaiting review.", "rev"), (
+ "There is {} new card due today.",
+ "There are {} new cards due today.", "new"), (
+ "There is {} new card in total.",
+ "There are {} new cards in total.", "newTotal"))
+ for singular, plural, concept in sentences:
+ stats[concept] = ngettext(singular, plural, stats[concept]).format(
+ stats[concept])
+ self.remText.setToolTip(_("Remaining cards
"
+ "{failed}
{rev}
{new}
{newTotal}
").format(**stats))
self.etaText.setText(_("ETA: %(timeLeft)s") % stats)
# retention & progress bars
p.setColor(QPalette.Base, QColor("black"))
@@ -218,7 +198,6 @@
self.setProgressColour(p, stats['dYesTotal%'])
self.progressBar.setPalette(p)
self.progressBar.setValue(stats['dYesTotal%'])
- # tooltips
tip = "
" + _("Performance") + "
"
tip += _("Click the bars to learn more.")
tip += "" + _("Reviews today") + "
"
@@ -281,25 +260,12 @@
self.setTimer("00:00")
def flashTimer(self):
- if not (self.main.deck.sessionStartTime and
- self.main.deck.sessionTimeLimit): # or self.main.deck.reviewEarly:
- return
- t = time.time() - self.main.deck.sessionStartTime
- t = self.main.deck.sessionTimeLimit - t
- if t < 0:
- t = 0
- self.setTimer('%02d:%02d' %
- (t/60, t%60))
self.timerFlashStart = time.time()
def updateCount(self):
- if self.main.inDbHandler:
- return
- if not self.main.deck:
+ if self.main.inDbHandler or not self.main.deck:
return
- if self.state in ("deckFinished", "studyScreen"):
- self.main.deck.updateCutoff()
- self.main.deck.reset()
+ elif self.state in ("deckFinished", "studyScreen"):
self.redraw()
self.main.updateTitleBar()
if self.state == "studyScreen":
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/ui/sync.py anki-1.2.8/ankiqt/ui/sync.py
--- anki-1.2.8.orig/ankiqt/ui/sync.py 2011-03-28 15:34:59.000000000 +0800
+++ anki-1.2.8/ankiqt/ui/sync.py 2012-04-01 14:38:35.606340660 +0800
@@ -147,17 +147,21 @@
"select modified, lastSync from decks").fetchone()
c.close()
except Exception, e:
+ print 'GENERAL except clause 1'
+ traceback.print_exc()
# we don't know which db library we're using, so do string match
if "locked" in unicode(e):
return
# unknown error
- self.error(e)
+ #self.error(e)
return -1
# ensure deck mods cached
try:
proxy = self.connect()
except SyncError, e:
- self.error(e)
+ print 'SyncError caught 1'
+ traceback.print_exc()
+ #self.error(e)
return -1
# exists on server?
deckCreated = False
@@ -171,7 +175,9 @@
proxy.createDeck(syncName)
deckCreated = True
except SyncError, e:
- self.error(e)
+ print 'SyncError caught 2'
+ traceback.print_exc()
+ #self.error(e)
return -1
# check conflicts
proxy.deckName = syncName
@@ -285,7 +291,9 @@
self.ok = False
if self.deck:
self.deck.close()
- self.error(e)
+ print 'GENERAL exception caught 2'
+ traceback.print_exc()
+ #self.error(e)
return -1
# Downloading personal decks
diff -Nurx '*.py~' anki-1.2.8.orig/ankiqt/ui/view.py anki-1.2.8/ankiqt/ui/view.py
--- anki-1.2.8.orig/ankiqt/ui/view.py 2011-03-28 15:33:31.000000000 +0800
+++ anki-1.2.8/ankiqt/ui/view.py 2012-04-01 02:03:31.564090228 +0800
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import anki, anki.utils
-from anki.sound import playFromText
+from anki.sound import playFromText, clearAudioQueue, stripSounds
from anki.utils import stripHTML
from anki.hooks import runHook, runFilter
from anki.media import stripMedia, escapeImages
@@ -18,7 +19,6 @@
failedCharColour = "#FF0000"
passedCharColour = "#00FF00"
-futureWarningColour = "#FF0000"
# Views - define the way a user is prompted for questions, etc
##########################################################################
@@ -45,7 +45,6 @@
elif self.state == "noDeck":
self.clearWindow()
self.drawWelcomeMessage()
- self.flush()
return
self.redisplay()
@@ -54,20 +53,14 @@
if self.state == "noDeck" or self.state == "studyScreen":
return
self.buffer = ""
- self.haveTop = self.needFutureWarning()
self.drawRule = (self.main.config['qaDivider'] and
self.main.currentCard and
not self.main.currentCard.cardModel.questionInAnswer)
- if not self.main.deck.isEmpty():
- if self.haveTop:
- self.drawTopSection()
if self.state == "showQuestion":
- self.setBackground()
self.drawQuestion()
if self.drawRule:
self.write("
")
elif self.state == "showAnswer":
- self.setBackground()
if not self.main.currentCard.cardModel.questionInAnswer:
self.drawQuestion(nosound=True)
if self.drawRule:
@@ -79,44 +72,20 @@
self.drawDeckFinishedMessage()
self.flush()
- def addStyles(self):
- # card styles
- s = ""
- return s
-
def clearWindow(self):
self.body.setHtml("")
self.buffer = ""
- def setBackground(self):
- col = self.main.currentCard.cardModel.lastFontColour
- self.write("" % col)
-
# Font properties & output
##########################################################################
def flush(self):
"Write the current HTML buffer to the screen."
- self.buffer = self.addStyles() + self.buffer
- # hook for user css
runHook("preFlushHook")
- self.buffer = '''%s%s''' % (
- getBase(self.main.deck, self.main.currentCard), self.buffer)
- #print self.buffer.encode("utf-8")
- b = self.buffer
- # Feeding webkit unicode can result in it not finding images, so on
- # linux/osx we percent escape the image paths as utf8. On Windows the
- # problem is more complicated - if we percent-escape as utf8 it fixes
- # some images but breaks others. When filenames are normalized by
- # dropbox they become unreadable if we escape them.
- if not sys.platform.startswith("win32"):
- # and self.main.config['mediaLocation'] == "dropbox"):
- b = escapeImages(b)
- self.body.setHtml(b)
+ t = time.time()
+ self.body.setHtml(self.buffer, self.main.deck.mediaQUrl())
+ t2 = time.time()
+ print 'flush: %f , setHtml took: %f' % (t, t2-t)
def write(self, text):
if type(text) != types.UnicodeType:
@@ -126,29 +95,16 @@
# Question and answer
##########################################################################
- def center(self, str, height=40):
- if not self.main.config['splitQA']:
- return "" + str + ""
- return '''\
-''' % (height, str)
-
def drawQuestion(self, nosound=False):
"Show the question."
if not self.main.config['splitQA']:
self.write("
")
- q = self.main.currentCard.htmlQuestion()
- if self.haveTop:
- height = 35
- elif self.main.currentCard.cardModel.questionInAnswer:
- height = 40
- else:
- height = 45
+ q = self.main.currentCard.getQuestion()
q = runFilter("drawQuestion", q, self.main.currentCard)
- self.write(self.center(self.mungeQA(self.main.deck, q), height))
+ self.write(stripSounds(q))
if (self.state != self.oldState and not nosound
and self.main.config['autoplaySounds']):
+ clearAudioQueue()
playFromText(q)
if self.main.currentCard.cardModel.typeAnswer:
self.adjustInputFont()
@@ -221,7 +177,7 @@
if tag == "equal":
lastEqual = b[i1:i2]
elif tag == "replace":
- ret += self.applyStyle(b[i1], lastEqual,
+ ret += self.applyStyle(b[i1], lastEqual,
b[i1:i2] + ("-" * ((j2 - j1) - (i2 - i1))))
lastEqual = ""
elif tag == "delete":
@@ -236,7 +192,7 @@
def drawAnswer(self):
"Show the answer."
- a = self.main.currentCard.htmlAnswer()
+ a = self.main.currentCard.getAnswer()
a = runFilter("drawAnswer", a, self.main.currentCard)
if self.main.currentCard.cardModel.typeAnswer:
try:
@@ -247,54 +203,19 @@
cor = ""
if cor:
given = unicode(self.main.typeAnswerField.text())
- res = self.correct(cor, given)
+ res = self.correct(ucd.normalize('NFC', cor),
+ ucd.normalize('NFC', given))
a = res + "
" + a
- self.write(self.center(''
- + self.mungeQA(self.main.deck, a)))
+ self.write(stripSounds(a))
if self.state != self.oldState and self.main.config['autoplaySounds']:
playFromText(a)
- def mungeQA(self, deck, txt):
- txt = mungeQA(deck, txt)
- # hack to fix thai presentation issues
- if self.main.config['addZeroSpace']:
- txt = txt.replace("", "")
- return txt
-
def onLoadFinished(self, bool):
if self.state == "showAnswer":
if self.main.config['scrollToAnswer']:
mf = self.body.page().mainFrame()
mf.evaluateJavaScript("location.hash = 'answer'")
- # Top section
- ##########################################################################
-
- def drawTopSection(self):
- "Show previous card, next scheduled time, and stats."
- self.buffer += ""
- self.drawFutureWarning()
- self.buffer += ""
-
- def needFutureWarning(self):
- if not self.main.currentCard:
- return
- if self.main.currentCard.due <= self.main.deck.dueCutoff:
- return
- if self.main.currentCard.due - time.time() <= self.main.deck.delay0:
- return
- if self.main.deck.scheduler == "cram":
- return
- return True
-
- def drawFutureWarning(self):
- if not self.needFutureWarning():
- return
- self.write("" % futureWarningColour +
- _("This card was due in %s.") % fmtTimeSpan(
- self.main.currentCard.due - time.time(), after=True) +
- "")
-
# Welcome/empty/finished deck messages
##########################################################################
@@ -333,8 +254,7 @@
def drawDeckFinishedMessage(self):
"Tell the user the deck is finished."
- self.main.mainWin.congratsLabel.setText(
- self.main.deck.deckFinishedMsg())
+ self.main.mainWin.congratsLabel.setText('You finished the deck for now.')
class AnkiWebView(QWebView):
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/cards.py anki-1.2.8/libanki/anki/cards.py
--- anki-1.2.8.orig/libanki/anki/cards.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/cards.py 2012-04-01 01:58:35.526622252 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -10,10 +11,12 @@
import time, sys, math, random
from anki.db import *
-from anki.models import CardModel, Model, FieldModel, formatQA
+from anki.models import CardModel, Model, FieldModel
from anki.facts import Fact, factsTable, Field
-from anki.utils import parseTags, findTag, stripHTML, genID, hexifyID
+from anki.utils import parseTags, findTag, stripHTML, genID, hexifyID, cleanName
from anki.media import updateMediaCount, mediaFiles
+from anki.template import render
+from anki.hooks import runFilter
MAX_TIMER = 60
@@ -80,7 +83,6 @@
self.type = 2
self.relativeDelay = self.type
self.timerStarted = False
- self.timerStopped = False
self.modified = time.time()
if created:
self.created = created
@@ -96,88 +98,78 @@
self.cardModelId = cardModel.id
self.ordinal = cardModel.ordinal
- def rebuildQA(self, deck, media=True):
- # format qa
- d = {}
- for f in self.fact.model.fieldModels:
- d[f.name] = (f.id, self.fact[f.name])
- qa = formatQA(None, self.fact.modelId, d, self.splitTags(),
- self.cardModel, deck)
- # find old media references
- files = {}
- for type in ("question", "answer"):
- for f in mediaFiles(getattr(self, type) or ""):
- if f in files:
- files[f] -= 1
- else:
- files[f] = -1
- # update q/a
- self.question = qa['question']
- self.answer = qa['answer']
- # determine media delta
- for type in ("question", "answer"):
- for f in mediaFiles(getattr(self, type)):
- if f in files:
- files[f] += 1
- else:
- files[f] = 1
- # update media counts if we're attached to deck
- if media:
- for (f, cnt) in files.items():
- updateMediaCount(deck, f, cnt)
- self.setModified()
-
def setModified(self):
self.modified = time.time()
def startTimer(self):
self.timerStarted = time.time()
- def stopTimer(self):
- self.timerStopped = time.time()
-
def thinkingTime(self):
- return (self.timerStopped or time.time()) - self.timerStarted
-
- def totalTime(self):
return time.time() - self.timerStarted
- def genFuzz(self):
- "Generate a random offset to spread intervals."
- self.fuzz = random.uniform(0.95, 1.05)
-
- def htmlQuestion(self, type="question", align=True):
- div = '''%s
''' % (
- type[0], type[0], hexifyID(self.cardModelId),
- getattr(self, type))
- # add outer div & alignment (with tables due to qt's html handling)
- if not align:
- return div
- attr = type + 'Align'
- if getattr(self.cardModel, attr) == 0:
- align = "center"
- elif getattr(self.cardModel, attr) == 1:
- align = "left"
- else:
- align = "right"
- return (("")
-
- def htmlAnswer(self, align=True):
- return self.htmlQuestion(type="answer", align=align)
+ def questionanswer(self, which):
+ t = time.time()
+ fields = self.fact.fieldsDict.copy()
+ t2 = time.time()
+ for key, value in fields.items():
+ if value:
+ fields[key] = u'%s' % (cleanName(key), value)
+ print 'questionanswer(%s) fieldsDict.copy took %f' % \
+ (which, t2 - t)
+ fields['tags'] = self.fact.tags
+ fields['Tags'] = self.fact.tags
+ fields['modelTags'] = self.fact.model.tags
+ fields['cardModel'] = self.cardModel.name
+ format = self.cardModel.qformat if which == 'question' else self.cardModel.aformat
+ # convert old style
+ #format = re.sub("%\((.+?)\)s", "{{\\1}}", format)
+ # allow custom rendering functions & info
+ kwargs = dict(qa=which, card=self)
+ fields = runFilter("prepareFields", fields, **kwargs)
+ format = runFilter("preRenderFormatQA", format, **kwargs)
+ html = render(format, fields)
+ html = runFilter("formatQA", html, **kwargs)
+ return html
+
+ def getQuestion(self):
+ return self.questionanswer('question')
+
+ def getAnswer(self):
+ return self.questionanswer('answer')
+
+ def state(self):
+ MATURE_THRESHOLD = 21 # is also in deck.py, sorry...
+ if self.reps == 0:
+ return "new"
+ elif self.interval > MATURE_THRESHOLD:
+ return "mature"
+ return "young"
+
+ @property
+ def isNew(self):
+ return self.relativeDelay == 2
+
+ @property
+ def isReview(self):
+ return self.relativeDelay == 1
+
+ @property
+ def isFailed(self):
+ return self.successive == 0 and self.reps > 0
- def updateStats(self, ease, state):
+ def updateStats(self, ease, totalTime):
self.reps += 1
if ease > 1:
self.successive += 1
else:
self.successive = 0
- delay = min(self.totalTime(), MAX_TIMER)
+ delay = min(totalTime, MAX_TIMER)
self.reviewTime += delay
if self.averageTime:
self.averageTime = (self.averageTime + delay) / 2.0
else:
self.averageTime = delay
+ state = self.state()
# we don't track first answer for cards
if state == "new":
state = "young"
@@ -192,9 +184,6 @@
self.firstAnswered = time.time()
self.setModified()
- def splitTags(self):
- return (self.fact.tags, self.fact.model.tags, self.cardModel.name)
-
def allTags(self):
"Non-canonified string of all tags."
return (self.fact.tags + "," +
@@ -203,92 +192,6 @@
def hasTag(self, tag):
return findTag(tag, parseTags(self.allTags()))
- def fromDB(self, s, id):
- r = s.first("""select
-id, factId, cardModelId, created, modified, tags, ordinal, question, answer,
-priority, interval, lastInterval, due, lastDue, factor,
-lastFactor, firstAnswered, reps, successive, averageTime, reviewTime,
-youngEase0, youngEase1, youngEase2, youngEase3, youngEase4,
-matureEase0, matureEase1, matureEase2, matureEase3, matureEase4,
-yesCount, noCount, spaceUntil, isDue, type, combinedDue
-from cards where id = :id""", id=id)
- if not r:
- return
- (self.id,
- self.factId,
- self.cardModelId,
- self.created,
- self.modified,
- self.tags,
- self.ordinal,
- self.question,
- self.answer,
- self.priority,
- self.interval,
- self.lastInterval,
- self.due,
- self.lastDue,
- self.factor,
- self.lastFactor,
- self.firstAnswered,
- self.reps,
- self.successive,
- self.averageTime,
- self.reviewTime,
- self.youngEase0,
- self.youngEase1,
- self.youngEase2,
- self.youngEase3,
- self.youngEase4,
- self.matureEase0,
- self.matureEase1,
- self.matureEase2,
- self.matureEase3,
- self.matureEase4,
- self.yesCount,
- self.noCount,
- self.spaceUntil,
- self.isDue,
- self.type,
- self.combinedDue) = r
- return True
-
- def toDB(self, s):
- "Write card to DB."
- s.execute("""update cards set
-modified=:modified,
-tags=:tags,
-interval=:interval,
-lastInterval=:lastInterval,
-due=:due,
-lastDue=:lastDue,
-factor=:factor,
-lastFactor=:lastFactor,
-firstAnswered=:firstAnswered,
-reps=:reps,
-successive=:successive,
-averageTime=:averageTime,
-reviewTime=:reviewTime,
-youngEase0=:youngEase0,
-youngEase1=:youngEase1,
-youngEase2=:youngEase2,
-youngEase3=:youngEase3,
-youngEase4=:youngEase4,
-matureEase0=:matureEase0,
-matureEase1=:matureEase1,
-matureEase2=:matureEase2,
-matureEase3=:matureEase3,
-matureEase4=:matureEase4,
-yesCount=:yesCount,
-noCount=:noCount,
-spaceUntil = :spaceUntil,
-isDue = 0,
-type = :type,
-combinedDue = :combinedDue,
-relativeDelay = :relativeDelay,
-priority = :priority
-where id=:id""", self.__dict__)
-
mapper(Card, cardsTable, properties={
'cardModel': relation(CardModel),
'fact': relation(Fact, backref="cards", primaryjoin=
@@ -298,6 +201,8 @@
mapper(Fact, factsTable, properties={
'model': relation(Model),
'fields': relation(Field, backref="fact", order_by=Field.ordinal),
+ 'fieldsDictFields': relation(Field,
+ collection_class=attribute_mapped_collection('fieldModel.name'))
})
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/db.py anki-1.2.8/libanki/anki/db.py
--- anki-1.2.8.orig/libanki/anki/db.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/db.py 2012-04-01 23:28:25.047975989 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -9,9 +10,6 @@
SessionHelper is a wrapper for the standard sqlalchemy session, which provides
some convenience routines, and manages transactions itself.
-object_session() is a replacement for the standard object_session(), which
-provides the features of SessionHelper, and avoids taking out another
-transaction.
"""
__docformat__ = 'restructuredtext'
@@ -27,17 +25,13 @@
ForeignKey, Boolean, String, Date,
UniqueConstraint, Index, PrimaryKeyConstraint)
from sqlalchemy import create_engine
-from sqlalchemy.orm import mapper, sessionmaker as _sessionmaker, relation, backref, \
- object_session as _object_session, class_mapper
+from sqlalchemy.orm import mapper, relation, backref, class_mapper, sessionmaker
from sqlalchemy.sql import select, text, and_
from sqlalchemy.exceptions import DBAPIError, OperationalError
-from sqlalchemy.pool import NullPool
-import sqlalchemy
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.orm.collections import attribute_mapped_collection
-# some users are still on 0.4.x..
-import warnings
-warnings.filterwarnings('ignore', 'Use session.add()')
-warnings.filterwarnings('ignore', 'Use session.expunge_all()')
+import sqlalchemy
# sqlalchemy didn't handle the move to unicodetext nicely
try:
@@ -55,39 +49,35 @@
class SessionHelper(object):
"Add some convenience routines to a session."
- def __init__(self, session, lock=False, transaction=True):
+ def __init__(self, session):
self._session = session
- self._lock = lock
- self._transaction = transaction
- if self._transaction:
- self._session.begin()
- if self._lock:
- self._lockDB()
- self._seen = True
-
- def save(self, obj):
- # compat
- if sqlalchemy.__version__.startswith("0.4."):
- self._session.save(obj)
- else:
- self._session.add(obj)
-
- def clear(self):
- # compat
- if sqlalchemy.__version__.startswith("0.4."):
- self._session.clear()
- else:
- self._session.expunge_all()
-
- def update(self, obj):
- # compat
- if sqlalchemy.__version__.startswith("0.4."):
- self._session.update(obj)
- else:
- self._session.add(obj)
+ self._lockDB()
def execute(self, *a, **ka):
+ sql = a[0]
+ if type(sql) == sqlalchemy.sql.expression._TextClause:
+ sql = sql.text
+ sql = sql.strip().upper()
+ if sql[-1] == ';':
+ sql = sql[:-1]
+ self._session.flush()
x = self._session.execute(*a, **ka)
+ expire = self._session.expire_all
+ if sql.startswith('CREATE TEMP TRIGGER') or sql.startswith('CREATE TEMPORARY TABLE'):
+ # turn a blind eye on those...
+ expire()
+ elif ';' in sql:
+ print 'WARNING: strange multi-statement SQL detected??'
+ print sql
+ expire()
+ elif sql.startswith('SELECT'):
+ pass
+ elif sql.startswith('PRAGMA'):
+ expire()
+ else:
+ print 'WARNING: bypassing the sqlalchemy session with writes enforces expire_all()'
+ print sql[:70] + '...'
+ expire()
runHook("dbFinished")
return x
@@ -121,29 +111,10 @@
return repr(self._session)
def commit(self):
+ print 'COMMIT()'
self._session.commit()
- if self._transaction:
- self._session.begin()
- if self._lock:
- self._lockDB()
+ self._lockDB()
def _lockDB(self):
"Take out a write lock."
self._session.execute(text("update decks set modified=modified"))
-
-def object_session(*args):
- s = _object_session(*args)
- if s:
- return SessionHelper(s, transaction=False)
- return None
-
-def sessionmaker(*args, **kwargs):
- if sqlalchemy.__version__ < "0.5":
- if 'autocommit' in kwargs:
- kwargs['transactional'] = not kwargs['autocommit']
- del kwargs['autocommit']
- else:
- if 'transactional' in kwargs:
- kwargs['autocommit'] = not kwargs['transactional']
- del kwargs['transactional']
- return _sessionmaker(*args, **kwargs)
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/deck.py anki-1.2.8/libanki/anki/deck.py
--- anki-1.2.8.orig/libanki/anki/deck.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/deck.py 2012-04-01 01:58:55.026718958 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -9,7 +10,8 @@
__docformat__ = 'restructuredtext'
import tempfile, time, os, random, sys, re, stat, shutil
-import types, traceback, simplejson, datetime
+import types, simplejson, datetime
+import PyQt4.QtCore
from anki.db import *
from anki.lang import _, ngettext
@@ -18,21 +20,20 @@
from anki.utils import parseTags, tidyHTML, genID, ids2str, hexifyID, \
canonifyTags, joinTags, addTags, checksum
from anki.history import CardHistoryEntry
-from anki.models import Model, CardModel, formatQA
-from anki.stats import dailyStats, globalStats, genToday
+from anki.models import Model, CardModel
+from anki.cards import Card
from anki.fonts import toPlatformFont
from anki.tags import initTagTables, tagIds
from operator import itemgetter
from itertools import groupby
from anki.hooks import runHook, hookEmpty
from anki.template import render
-from anki.media import updateMediaCount, mediaFiles, \
- rebuildMediaDir
+from anki.media import updateMediaCount, mediaFiles, rebuildMediaDir
import anki.latex # sets up hook
# ensure all the DB metadata in other files is loaded before proceeding
import anki.models, anki.facts, anki.cards, anki.stats
-import anki.history, anki.media
+import anki.history, anki.media, anki.schedulers
# the current code set type -= 3 for manually suspended cards, and += 3*n
# for temporary suspends, (where n=1 for bury, n=2 for review/cram).
@@ -140,851 +141,27 @@
class Deck(object):
"Top-level object. Manages facts, cards and scheduling information."
- factorFour = 1.3
- initialFactor = 2.5
- minimumAverage = 1.7
- maxScheduleTime = 36500
-
- def __init__(self, path=None):
- "Create a new deck."
- # a limit of 1 deck in the table
- self.id = 1
- # db session factory and instance
- self.Session = None
- self.s = None
-
- def _initVars(self):
- self.tmpMediaDir = None
- self.mediaPrefix = ""
- self.lastTags = u""
- self.lastLoaded = time.time()
- self.undoEnabled = False
- self.sessionStartReps = 0
- self.sessionStartTime = 0
- self.lastSessionStart = 0
- self.queueLimit = 200
- # if most recent deck var not defined, make sure defaults are set
- if not self.s.scalar("select 1 from deckVars where key = 'revSpacing'"):
- self.setVarDefault("suspendLeeches", True)
- self.setVarDefault("leechFails", 16)
- self.setVarDefault("perDay", True)
- self.setVarDefault("newActive", "")
- self.setVarDefault("revActive", "")
- self.setVarDefault("newInactive", self.suspended)
- self.setVarDefault("revInactive", self.suspended)
- self.setVarDefault("newSpacing", 60)
- self.setVarDefault("mediaURL", "")
- self.setVarDefault("latexPre", """\
-\\documentclass[12pt]{article}
-\\special{papersize=3in,5in}
-\\usepackage[utf8]{inputenc}
-\\usepackage{amssymb,amsmath}
-\\pagestyle{empty}
-\\setlength{\\parindent}{0in}
-\\begin{document}
-""")
- self.setVarDefault("latexPost", "\\end{document}")
- self.setVarDefault("revSpacing", 0.1)
- self.updateCutoff()
- self.setupStandardScheduler()
-
def modifiedSinceSave(self):
return self.modified > self.lastLoaded
- # Queue management
- ##########################################################################
-
- def setupStandardScheduler(self):
- self.getCardId = self._getCardId
- self.fillFailedQueue = self._fillFailedQueue
- self.fillRevQueue = self._fillRevQueue
- self.fillNewQueue = self._fillNewQueue
- self.rebuildFailedCount = self._rebuildFailedCount
- self.rebuildRevCount = self._rebuildRevCount
- self.rebuildNewCount = self._rebuildNewCount
- self.requeueCard = self._requeueCard
- self.timeForNewCard = self._timeForNewCard
- self.updateNewCountToday = self._updateNewCountToday
- self.cardQueue = self._cardQueue
- self.finishScheduler = None
- self.answerCard = self._answerCard
- self.cardLimit = self._cardLimit
- self.answerPreSave = None
- self.spaceCards = self._spaceCards
- self.scheduler = "standard"
- # restore any cards temporarily suspended by alternate schedulers
- try:
- self.resetAfterReviewEarly()
- except OperationalError, e:
- # will fail if deck hasn't been upgraded yet
- pass
-
- def fillQueues(self):
- self.fillFailedQueue()
- self.fillRevQueue()
- self.fillNewQueue()
-
- def rebuildCounts(self):
- # global counts
- self.cardCount = self.s.scalar("select count(*) from cards")
- self.factCount = self.s.scalar("select count(*) from facts")
- # due counts
- self.rebuildFailedCount()
- self.rebuildRevCount()
- self.rebuildNewCount()
-
- def _cardLimit(self, active, inactive, sql):
- yes = parseTags(self.getVar(active))
- no = parseTags(self.getVar(inactive))
- if yes:
- yids = tagIds(self.s, yes).values()
- nids = tagIds(self.s, no).values()
- return sql.replace(
- "where",
- "where +c.id in (select cardId from cardTags where "
- "tagId in %s) and +c.id not in (select cardId from "
- "cardTags where tagId in %s) and" % (
- ids2str(yids),
- ids2str(nids)))
- elif no:
- nids = tagIds(self.s, no).values()
- return sql.replace(
- "where",
- "where +c.id not in (select cardId from cardTags where "
- "tagId in %s) and" % ids2str(nids))
- else:
- return sql
-
- def _rebuildFailedCount(self):
- # This is a count of all failed cards within the current day cutoff.
- # The cards may not be ready for review yet, but can still be
- # displayed if failedCardsMax is reached.
- self.failedSoonCount = self.s.scalar(
- self.cardLimit(
- "revActive", "revInactive",
- "select count(*) from cards c where type = 0 "
- "and combinedDue < :lim"), lim=self.failedCutoff)
-
- def _rebuildRevCount(self):
- self.revCount = self.s.scalar(
- self.cardLimit(
- "revActive", "revInactive",
- "select count(*) from cards c where type = 1 "
- "and combinedDue < :lim"), lim=self.dueCutoff)
-
- def _rebuildNewCount(self):
- self.newCount = self.s.scalar(
- self.cardLimit(
- "newActive", "newInactive",
- "select count(*) from cards c where type = 2 "
- "and combinedDue < :lim"), lim=self.dueCutoff)
- self.updateNewCountToday()
- self.spacedCards = []
-
- def _updateNewCountToday(self):
- self.newCountToday = max(min(
- self.newCount, self.newCardsPerDay -
- self.newCardsDoneToday()), 0)
-
- def _fillFailedQueue(self):
- if self.failedSoonCount and not self.failedQueue:
- self.failedQueue = self.s.all(
- self.cardLimit(
- "revActive", "revInactive", """
-select c.id, factId, combinedDue from cards c where
-type = 0 and combinedDue < :lim order by combinedDue
-limit %d""" % self.queueLimit), lim=self.failedCutoff)
- self.failedQueue.reverse()
-
- def _fillRevQueue(self):
- if self.revCount and not self.revQueue:
- self.revQueue = self.s.all(
- self.cardLimit(
- "revActive", "revInactive", """
-select c.id, factId from cards c where
-type = 1 and combinedDue < :lim order by %s
-limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dueCutoff)
- self.revQueue.reverse()
-
- def _fillNewQueue(self):
- if self.newCountToday and not self.newQueue and not self.spacedCards:
- self.newQueue = self.s.all(
- self.cardLimit(
- "newActive", "newInactive", """
-select c.id, factId from cards c where
-type = 2 and combinedDue < :lim order by %s
-limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dueCutoff)
- self.newQueue.reverse()
-
- def queueNotEmpty(self, queue, fillFunc, new=False):
- while True:
- self.removeSpaced(queue, new)
- if queue:
- return True
- fillFunc()
- if not queue:
- return False
-
- def removeSpaced(self, queue, new=False):
- popped = []
- delay = None
- while queue:
- fid = queue[-1][1]
- if fid in self.spacedFacts:
- # still spaced
- id = queue.pop()[0]
- # assuming 10 cards/minute, track id if likely to expire
- # before queue refilled
- if new and self.newSpacing < self.queueLimit * 6:
- popped.append(id)
- delay = self.spacedFacts[fid]
- else:
- if popped:
- self.spacedCards.append((delay, popped))
- return
-
- def revNoSpaced(self):
- return self.queueNotEmpty(self.revQueue, self.fillRevQueue)
-
- def newNoSpaced(self):
- return self.queueNotEmpty(self.newQueue, self.fillNewQueue, True)
-
- def _requeueCard(self, card, oldSuc):
- newType = None
- try:
- if card.reps == 1:
- if self.newFromCache:
- # fetched from spaced cache
- newType = 2
- cards = self.spacedCards.pop(0)[1]
- # reschedule the siblings
- if len(cards) > 1:
- self.spacedCards.append(
- (time.time() + self.newSpacing, cards[1:]))
- else:
- # fetched from normal queue
- newType = 1
- self.newQueue.pop()
- elif oldSuc == 0:
- self.failedQueue.pop()
- else:
- self.revQueue.pop()
- except:
- raise Exception("""\
-requeueCard() failed. Please report this along with the steps you take to
-produce the problem.
-
-Counts %d %d %d
-Queue %d %d %d
-Card info: %d %d %d
-New type: %s""" % (self.failedSoonCount, self.revCount, self.newCountToday,
- len(self.failedQueue), len(self.revQueue),
- len(self.newQueue),
- card.reps, card.successive, oldSuc, `newType`))
-
- def revOrder(self):
- return ("priority desc, interval desc",
- "priority desc, interval",
- "priority desc, combinedDue",
- "priority desc, factId, ordinal")[self.revCardOrder]
-
- def newOrder(self):
- return ("priority desc, due",
- "priority desc, due",
- "priority desc, due desc")[self.newCardOrder]
-
- def rebuildTypes(self):
- "Rebuild the type cache. Only necessary on upgrade."
- # set canonical type first
- self.s.statement("""
-update cards set
-relativeDelay = (case
-when successive then 1 when reps then 0 else 2 end)
-""")
- # then current type based on that
- self.s.statement("""
-update cards set
-type = (case
-when type >= 0 then relativeDelay else relativeDelay - 3 end)
-""")
-
- def _cardQueue(self, card):
- return self.cardType(card)
-
- def cardType(self, card):
- "Return the type of the current card (what queue it's in)"
- if card.successive:
- return 1
- elif card.reps:
- return 0
- else:
- return 2
-
- def updateCutoff(self):
- d = datetime.datetime.utcfromtimestamp(
- time.time() - self.utcOffset) + datetime.timedelta(days=1)
- d = datetime.datetime(d.year, d.month, d.day)
- newday = self.utcOffset - time.timezone
- d += datetime.timedelta(seconds=newday)
- cutoff = time.mktime(d.timetuple())
- # cutoff must not be in the past
- while cutoff < time.time():
- cutoff += 86400
- # cutoff must not be more than 24 hours in the future
- cutoff = min(time.time() + 86400, cutoff)
- self.failedCutoff = cutoff
- if self.getBool("perDay"):
- self.dueCutoff = cutoff
- else:
- self.dueCutoff = time.time()
-
- def reset(self):
- # setup global/daily stats
- self._globalStats = globalStats(self)
- self._dailyStats = dailyStats(self)
- # recheck counts
- self.rebuildCounts()
- # empty queues; will be refilled by getCard()
- self.failedQueue = []
- self.revQueue = []
- self.newQueue = []
- self.spacedFacts = {}
- # determine new card distribution
- if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
- if self.newCountToday:
- self.newCardModulus = (
- (self.newCountToday + self.revCount) / self.newCountToday)
- # if there are cards to review, ensure modulo >= 2
- if self.revCount:
- self.newCardModulus = max(2, self.newCardModulus)
- else:
- self.newCardModulus = 0
- else:
- self.newCardModulus = 0
- # recache css
- self.rebuildCSS()
- # spacing for delayed cards - not to be confused with newCardSpacing
- # above
- self.newSpacing = self.getFloat('newSpacing')
- self.revSpacing = self.getFloat('revSpacing')
-
- def checkDay(self):
- # check if the day has rolled over
- if genToday(self) != self._dailyStats.day:
- self.updateCutoff()
- self.reset()
-
- # Review early
- ##########################################################################
-
- def setupReviewEarlyScheduler(self):
- self.fillRevQueue = self._fillRevEarlyQueue
- self.rebuildRevCount = self._rebuildRevEarlyCount
- self.finishScheduler = self._onReviewEarlyFinished
- self.answerPreSave = self._reviewEarlyPreSave
- self.scheduler = "reviewEarly"
-
- def _reviewEarlyPreSave(self, card, ease):
- if ease > 1:
- # prevent it from appearing in next queue fill
- card.type += 6
-
- def resetAfterReviewEarly(self):
- "Put temporarily suspended cards back into play. Caller must .reset()"
- # FIXME: can ignore priorities in the future
- ids = self.s.column0(
- "select id from cards where type between 6 and 8 or priority = -1")
- if ids:
- self.updatePriorities(ids)
- self.s.statement(
- "update cards set type = type - 6 where type between 6 and 8")
- self.flushMod()
-
- def _onReviewEarlyFinished(self):
- # clean up buried cards
- self.resetAfterReviewEarly()
- # and go back to regular scheduler
- self.setupStandardScheduler()
-
- def _rebuildRevEarlyCount(self):
- # in the future it would be nice to skip the first x days of due cards
- self.revCount = self.s.scalar(
- self.cardLimit(
- "revActive", "revInactive", """
-select count() from cards c where type = 1 and combinedDue > :now
-"""), now=self.dueCutoff)
-
- def _fillRevEarlyQueue(self):
- if self.revCount and not self.revQueue:
- self.revQueue = self.s.all(
- self.cardLimit(
- "revActive", "revInactive", """
-select id, factId from cards c where type = 1 and combinedDue > :lim
-order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
- self.revQueue.reverse()
-
- # Learn more
- ##########################################################################
-
- def setupLearnMoreScheduler(self):
- self.rebuildNewCount = self._rebuildLearnMoreCount
- self.updateNewCountToday = self._updateLearnMoreCountToday
- self.finishScheduler = self.setupStandardScheduler
- self.scheduler = "learnMore"
-
- def _rebuildLearnMoreCount(self):
- self.newCount = self.s.scalar(
- self.cardLimit(
- "newActive", "newInactive",
- "select count(*) from cards c where type = 2 "
- "and combinedDue < :lim"), lim=self.dueCutoff)
- self.spacedCards = []
-
- def _updateLearnMoreCountToday(self):
- self.newCountToday = self.newCount
-
- # Cramming
- ##########################################################################
-
- def setupCramScheduler(self, active, order):
- self.getCardId = self._getCramCardId
- self.activeCramTags = active
- self.cramOrder = order
- self.rebuildNewCount = self._rebuildCramNewCount
- self.rebuildRevCount = self._rebuildCramCount
- self.rebuildFailedCount = self._rebuildFailedCramCount
- self.fillRevQueue = self._fillCramQueue
- self.fillFailedQueue = self._fillFailedCramQueue
- self.finishScheduler = self.setupStandardScheduler
- self.failedCramQueue = []
- self.requeueCard = self._requeueCramCard
- self.cardQueue = self._cramCardQueue
- self.answerCard = self._answerCramCard
- self.spaceCards = self._spaceCramCards
- # reuse review early's code
- self.answerPreSave = self._cramPreSave
- self.cardLimit = self._cramCardLimit
- self.scheduler = "cram"
-
- def _cramPreSave(self, card, ease):
- # prevent it from appearing in next queue fill
- card.lastInterval = self.cramLastInterval
- card.type += 6
-
- def _spaceCramCards(self, card):
- self.spacedFacts[card.factId] = time.time() + self.newSpacing
-
- def _answerCramCard(self, card, ease):
- self.cramLastInterval = card.lastInterval
- self._answerCard(card, ease)
- if ease == 1:
- self.failedCramQueue.insert(0, [card.id, card.factId])
-
- def _getCramCardId(self, check=True):
- self.checkDay()
- self.fillQueues()
- if self.failedCardMax and self.failedSoonCount >= self.failedCardMax:
- return self.failedQueue[-1][0]
- # card due for review?
- if self.revNoSpaced():
- return self.revQueue[-1][0]
- if self.failedQueue:
- return self.failedQueue[-1][0]
- if check:
- # collapse spaced cards before reverting back to old scheduler
- self.reset()
- return self.getCardId(False)
- # if we're in a custom scheduler, we may need to switch back
- if self.finishScheduler:
- self.finishScheduler()
- self.reset()
- return self.getCardId()
-
- def _cramCardQueue(self, card):
- if self.revQueue and self.revQueue[-1][0] == card.id:
- return 1
- else:
- return 0
-
- def _requeueCramCard(self, card, oldSuc):
- if self.cardQueue(card) == 1:
- self.revQueue.pop()
- else:
- self.failedCramQueue.pop()
-
- def _rebuildCramNewCount(self):
- self.newCount = 0
- self.newCountToday = 0
-
- def _cramCardLimit(self, active, inactive, sql):
- # inactive is (currently) ignored
- if isinstance(active, list):
- return sql.replace(
- "where", "where +c.id in " + ids2str(active) + " and")
- else:
- yes = parseTags(active)
- if yes:
- yids = tagIds(self.s, yes).values()
- return sql.replace(
- "where ",
- "where +c.id in (select cardId from cardTags where "
- "tagId in %s) and " % ids2str(yids))
- else:
- return sql
-
- def _fillCramQueue(self):
- if self.revCount and not self.revQueue:
- self.revQueue = self.s.all(self.cardLimit(
- self.activeCramTags, "", """
-select id, factId from cards c
-where type between 0 and 2
-order by %s
-limit %s""" % (self.cramOrder, self.queueLimit)))
- self.revQueue.reverse()
-
- def _rebuildCramCount(self):
- self.revCount = self.s.scalar(self.cardLimit(
- self.activeCramTags, "",
- "select count(*) from cards c where type between 0 and 2"))
-
- def _rebuildFailedCramCount(self):
- self.failedSoonCount = len(self.failedCramQueue)
-
- def _fillFailedCramQueue(self):
- self.failedQueue = self.failedCramQueue
-
- # Getting the next card
- ##########################################################################
-
- def getCard(self, orm=True):
+ def getCard(self):
"Return the next card object, or None."
- id = self.getCardId()
- if id:
- return self.cardFromId(id, orm)
- else:
- self.stopSession()
-
- def _getCardId(self, check=True):
- "Return the next due card id, or None."
- self.checkDay()
- self.fillQueues()
- self.updateNewCountToday()
- if self.failedQueue:
- # failed card due?
- if self.delay0:
- if self.failedQueue[-1][2] + self.delay0 < time.time():
- return self.failedQueue[-1][0]
- # failed card queue too big?
- if (self.failedCardMax and
- self.failedSoonCount >= self.failedCardMax):
- return self.failedQueue[-1][0]
- # distribute new cards?
- if self.newNoSpaced() and self.timeForNewCard():
- return self.getNewCard()
- # card due for review?
- if self.revNoSpaced():
- return self.revQueue[-1][0]
- # new cards left?
- if self.newCountToday:
- id = self.getNewCard()
- if id:
- return id
- if check:
- # check for expired cards, or new day rollover
- self.updateCutoff()
- self.reset()
- return self.getCardId(check=False)
- # display failed cards early/last
- if not check and self.showFailedLast() and self.failedQueue:
- return self.failedQueue[-1][0]
- # if we're in a custom scheduler, we may need to switch back
- if self.finishScheduler:
- self.finishScheduler()
- self.reset()
- return self.getCardId()
-
- # Get card: helper functions
- ##########################################################################
+ return self._scheduler.getCard()
- def _timeForNewCard(self):
- "True if it's time to display a new card when distributing."
- if not self.newCountToday:
- return False
- if self.newCardSpacing == NEW_CARDS_LAST:
- return False
- if self.newCardSpacing == NEW_CARDS_FIRST:
- return True
- # force review if there are very high priority cards
- if self.revQueue:
- if self.s.scalar(
- "select 1 from cards where id = :id and priority = 4",
- id = self.revQueue[-1][0]):
- return False
- if self.newCardModulus:
- return self._dailyStats.reps % self.newCardModulus == 0
- else:
- return False
-
- def getNewCard(self):
- src = None
- if (self.spacedCards and
- self.spacedCards[0][0] < time.time()):
- # spaced card has expired
- src = 0
- elif self.newQueue:
- # card left in new queue
- src = 1
- elif self.spacedCards:
- # card left in spaced queue
- src = 0
- else:
- # only cards spaced to another day left
- return
- if src == 0:
- cards = self.spacedCards[0][1]
- self.newFromCache = True
- return cards[0]
- else:
- self.newFromCache = False
- return self.newQueue[-1][0]
-
- def showFailedLast(self):
- return self.collapseTime or not self.delay0
-
- def cardFromId(self, id, orm=False):
- "Given a card ID, return a card, and start the card timer."
- if orm:
- card = self.s.query(anki.cards.Card).get(id)
- if not card:
- return
- card.timerStopped = False
- else:
- card = anki.cards.Card()
- if not card.fromDB(self.s, id):
- return
- card.deck = self
- card.genFuzz()
- card.startTimer()
- return card
-
- # Answering a card
- ##########################################################################
-
- def _answerCard(self, card, ease):
+ def answerCard(self, card, ease, reviewTime):
undoName = _("Answer Card")
self.setUndoStart(undoName)
- now = time.time()
- # old state
- oldState = self.cardState(card)
- oldQueue = self.cardQueue(card)
- lastDelaySecs = time.time() - card.combinedDue
- lastDelay = lastDelaySecs / 86400.0
- oldSuc = card.successive
- # update card details
- last = card.interval
- card.interval = self.nextInterval(card, ease)
- card.lastInterval = last
- if card.reps:
- # only update if card was not new
- card.lastDue = card.due
- card.due = self.nextDue(card, ease, oldState)
- card.isDue = 0
- card.lastFactor = card.factor
- card.spaceUntil = 0
- if not self.finishScheduler:
- # don't update factor in custom schedulers
- self.updateFactor(card, ease)
- # spacing
- self.spaceCards(card)
- # adjust counts for current card
- if ease == 1:
- if card.due < self.failedCutoff:
- self.failedSoonCount += 1
- if oldQueue == 0:
- self.failedSoonCount -= 1
- elif oldQueue == 1:
- self.revCount -= 1
- else:
- self.newCount -= 1
- # card stats
- anki.cards.Card.updateStats(card, ease, oldState)
- # update type & ensure past cutoff
- card.type = self.cardType(card)
- card.relativeDelay = card.type
- if ease != 1:
- card.due = max(card.due, self.dueCutoff+1)
- # allow custom schedulers to munge the card
- if self.answerPreSave:
- self.answerPreSave(card, ease)
- # save
- card.combinedDue = card.due
- card.toDB(self.s)
- # global/daily stats
- anki.stats.updateAllStats(self.s, self._globalStats, self._dailyStats,
- card, ease, oldState)
- # review history
- entry = CardHistoryEntry(card, ease, lastDelay)
- entry.writeSQL(self.s)
- self.modified = now
- # remove from queue
- self.requeueCard(card, oldSuc)
- # leech handling - we need to do this after the queue, as it may cause
- # a reset()
- isLeech = self.isLeech(card)
- if isLeech:
- self.handleLeech(card)
- runHook("cardAnswered", card.id, isLeech)
+ lastDelay = (time.time() - card.combinedDue) / 86400.0
+ self._scheduler.updateAnsweredCard(card, ease)
+ card.updateStats(ease, reviewTime)
+ anki.stats.updateAllStats(self.s, card, ease)
+ self.s.add(CardHistoryEntry(card, ease, lastDelay))
+ self.setModified()
+ runHook("cardAnswered", card.id, False) # last argument was “isLeech”
self.setUndoEnd(undoName)
- def _spaceCards(self, card):
- new = time.time() + self.newSpacing
- self.s.statement("""
-update cards set
-combinedDue = (case
-when type = 1 then combinedDue + 86400 * (case
- when interval*:rev < 1 then 0
- else interval*:rev
- end)
-when type = 2 then :new
-end),
-modified = :now, isDue = 0
-where id != :id and factId = :factId
-and combinedDue < :cut
-and type between 1 and 2""",
- id=card.id, now=time.time(), factId=card.factId,
- cut=self.dueCutoff, new=new, rev=self.revSpacing)
- # update local cache of seen facts
- self.spacedFacts[card.factId] = new
-
- def isLeech(self, card):
- no = card.noCount
- fmax = self.getInt('leechFails')
- if not fmax:
- return
- return (
- # failed
- not card.successive and
- # greater than fail threshold
- no >= fmax and
- # at least threshold/2 reps since last time
- (fmax - no) % (max(fmax/2, 1)) == 0)
-
- def handleLeech(self, card):
- self.refreshSession()
- scard = self.cardFromId(card.id, True)
- tags = scard.fact.tags
- tags = addTags("Leech", tags)
- scard.fact.tags = canonifyTags(tags)
- scard.fact.setModified(textChanged=True, deck=self)
- self.updateFactTags([scard.fact.id])
- self.s.flush()
- self.s.expunge(scard)
- if self.getBool('suspendLeeches'):
- self.suspendCards([card.id])
- self.reset()
- self.refreshSession()
-
- # Interval management
- ##########################################################################
-
def nextInterval(self, card, ease):
- "Return the next interval for CARD given EASE."
- delay = self._adjustedDelay(card, ease)
- return self._nextInterval(card, delay, ease)
-
- def _nextInterval(self, card, delay, ease):
- interval = card.interval
- factor = card.factor
- # if shown early
- if delay < 0:
- # FIXME: this should recreate lastInterval from interval /
- # lastFactor, or we lose delay information when reviewing early
- interval = max(card.lastInterval, card.interval + delay)
- if interval < self.midIntervalMin:
- interval = 0
- delay = 0
- # if interval is less than mid interval, use presets
- if ease == 1:
- interval *= self.delay2
- if interval < self.hardIntervalMin:
- interval = 0
- elif interval == 0:
- if ease == 2:
- interval = random.uniform(self.hardIntervalMin,
- self.hardIntervalMax)
- elif ease == 3:
- interval = random.uniform(self.midIntervalMin,
- self.midIntervalMax)
- elif ease == 4:
- interval = random.uniform(self.easyIntervalMin,
- self.easyIntervalMax)
- else:
- # if not cramming, boost initial 2
- if (interval < self.hardIntervalMax and
- interval > 0.166):
- mid = (self.midIntervalMin + self.midIntervalMax) / 2.0
- interval = mid / factor
- # multiply last interval by factor
- if ease == 2:
- interval = (interval + delay/4) * 1.2
- elif ease == 3:
- interval = (interval + delay/2) * factor
- elif ease == 4:
- interval = (interval + delay) * factor * self.factorFour
- fuzz = random.uniform(0.95, 1.05)
- interval *= fuzz
- if self.maxScheduleTime:
- interval = min(interval, self.maxScheduleTime)
- return interval
-
- def nextIntervalStr(self, card, ease, short=False):
- "Return the next interval for CARD given EASE as a string."
- int = self.nextInterval(card, ease)
- return anki.utils.fmtTimeSpan(int*86400, short=short)
-
- def nextDue(self, card, ease, oldState):
- "Return time when CARD will expire given EASE."
- if ease == 1:
- # 600 is a magic value which means no bonus, and is used to ease
- # upgrades
- cram = self.scheduler == "cram"
- if (not cram and oldState == "mature"
- and self.delay1 and self.delay1 != 600):
- # user wants a bonus of 1+ days. put the failed cards at the
- # start of the future day, so that failures that day will come
- # after the waiting cards
- return self.failedCutoff + (self.delay1 - 1)*86400
- else:
- due = 0
- else:
- due = card.interval * 86400.0
- return due + time.time()
-
- def updateFactor(self, card, ease):
- "Update CARD's factor based on EASE."
- card.lastFactor = card.factor
- if not card.reps:
- # card is new, inherit beginning factor
- card.factor = self.averageFactor
- if card.successive and not self.cardIsBeingLearnt(card):
- if ease == 1:
- card.factor -= 0.20
- elif ease == 2:
- card.factor -= 0.15
- if ease == 4:
- card.factor += 0.10
- card.factor = max(1.3, card.factor)
-
- def _adjustedDelay(self, card, ease):
- "Return an adjusted delay value for CARD based on EASE."
- if self.cardIsNew(card):
- return 0
- if card.reps and not card.successive:
- return 0
- if card.combinedDue <= self.dueCutoff:
- return (self.dueCutoff - card.due) / 86400.0
- else:
- return (self.dueCutoff - card.combinedDue) / 86400.0
+ return self._scheduler.nextInterval(card, ease)
def resetCards(self, ids):
"Reset progress on cards in IDS."
@@ -994,8 +171,8 @@
youngEase0 = 0, youngEase1 = 0, youngEase2 = 0, youngEase3 = 0,
youngEase4 = 0, matureEase0 = 0, matureEase1 = 0, matureEase2 = 0,
matureEase3 = 0,matureEase4 = 0, yesCount = 0, noCount = 0,
-spaceUntil = 0, type = 2, relativeDelay = 2,
-combinedDue = created, modified = :now, due = created, isDue = 0
+type = 2, relativeDelay = 2,
+combinedDue = created, modified = :now, due = created
where id in %s""" % ids2str(ids), now=time.time(), new=0)
if self.newCardOrder == NEW_CARDS_RANDOM:
# we need to re-randomize now
@@ -1052,192 +229,15 @@
yesCount = 1,
firstAnswered = :t,
type = 1,
-relativeDelay = 1,
-isDue = 0
+relativeDelay = 1
where id = :id""", vals)
self.flushMod()
- # Times
- ##########################################################################
-
- def nextDueMsg(self):
- next = self.earliestTime()
- if next:
- # all new cards except suspended
- newCount = self.newCardsDueBy(self.dueCutoff + 86400)
- newCardsTomorrow = min(newCount, self.newCardsPerDay)
- cards = self.cardsDueBy(self.dueCutoff + 86400)
- msg = _('''\
-
-At this time tomorrow:
-%(wait)s
-%(new)s''') % {
- 'new': ngettext("There will be %d new card.",
- "There will be %d new cards.",
- newCardsTomorrow) % newCardsTomorrow,
- 'wait': ngettext("There will be %s review.",
- "There will be %s reviews.", cards) % cards,
- }
- if next > (self.dueCutoff+86400) and not newCardsTomorrow:
- msg = (_("The next review is in %s.") %
- self.earliestTimeStr())
- else:
- msg = _("No cards are due.")
- return msg
-
- def earliestTime(self):
- """Return the time of the earliest card.
-This may be in the past if the deck is not finished.
-If the deck has no (enabled) cards, return None.
-Ignore new cards."""
- earliestRev = self.s.scalar(self.cardLimit("revActive", "revInactive", """
-select combinedDue from cards c where type = 1
-order by combinedDue
-limit 1"""))
- earliestFail = self.s.scalar(self.cardLimit("revActive", "revInactive", """
-select combinedDue+%d from cards c where type = 0
-order by combinedDue
-limit 1""" % self.delay0))
- if earliestRev and earliestFail:
- return min(earliestRev, earliestFail)
- elif earliestRev:
- return earliestRev
- else:
- return earliestFail
-
- def earliestTimeStr(self, next=None):
- """Return the relative time to the earliest card as a string."""
- if next == None:
- next = self.earliestTime()
- if not next:
- return _("unknown")
- diff = next - time.time()
- return anki.utils.fmtTimeSpan(diff)
-
- def cardsDueBy(self, time):
- "Number of cards due at TIME. Ignore new cards"
- return self.s.scalar(
- self.cardLimit(
- "revActive", "revInactive",
- "select count(*) from cards c where type between 0 and 1 "
- "and combinedDue < :lim"), lim=time)
-
- def newCardsDueBy(self, time):
- "Number of new cards due at TIME."
- return self.s.scalar(
- self.cardLimit(
- "newActive", "newInactive",
- "select count(*) from cards c where type = 2 "
- "and combinedDue < :lim"), lim=time)
-
- def deckFinishedMsg(self):
- spaceSusp = ""
- c= self.spacedCardCount()
- if c:
- spaceSusp += ngettext(
- 'There is %d delayed card.',
- 'There are %d delayed cards.', c) % c
- c2 = self.hiddenCards()
- if c2:
- if spaceSusp:
- spaceSusp += "
"
- spaceSusp += _(
- "Some cards are inactive or suspended.")
- if spaceSusp:
- spaceSusp = "
" + spaceSusp
- return _('''\
-
-
Congratulations!
You have finished for now.
-%(next)s
-%(spaceSusp)s
-''') % {
- "next": self.nextDueMsg(),
- "spaceSusp": spaceSusp,
- }
-
- # Priorities
- ##########################################################################
-
- def updateAllPriorities(self, partial=False, dirty=True):
- "Update all card priorities if changed. Caller must .reset()"
- new = self.updateTagPriorities()
- if not partial:
- new = self.s.all("select id, priority as pri from tags")
- cids = self.s.column0(
- "select distinct cardId from cardTags where tagId in %s" %
- ids2str([x['id'] for x in new]))
- self.updatePriorities(cids, dirty=dirty)
-
- def updateTagPriorities(self):
- "Update priority setting on tags table."
- # make sure all priority tags exist
- for s in (self.lowPriority, self.medPriority,
- self.highPriority):
- tagIds(self.s, parseTags(s))
- tags = self.s.all("select tag, id, priority from tags")
- tags = [(x[0].lower(), x[1], x[2]) for x in tags]
- up = {}
- for (type, pri) in ((self.lowPriority, 1),
- (self.medPriority, 3),
- (self.highPriority, 4)):
- for tag in parseTags(type.lower()):
- up[tag] = pri
- new = []
- for (tag, id, pri) in tags:
- if tag in up and up[tag] != pri:
- new.append({'id': id, 'pri': up[tag]})
- elif tag not in up and pri != 2:
- new.append({'id': id, 'pri': 2})
- self.s.statements(
- "update tags set priority = :pri where id = :id",
- new)
- return new
-
- def updatePriorities(self, cardIds, suspend=[], dirty=True):
- "Update priorities for cardIds. Caller must .reset()."
- # any tags to suspend
- if suspend:
- ids = tagIds(self.s, suspend)
- self.s.statement(
- "update tags set priority = 0 where id in %s" %
- ids2str(ids.values()))
- if len(cardIds) > 1000:
- limit = ""
- else:
- limit = "and cardTags.cardId in %s" % ids2str(cardIds)
- cards = self.s.all("""
-select cardTags.cardId,
-case
-when max(tags.priority) > 2 then max(tags.priority)
-when min(tags.priority) = 1 then 1
-else 2 end
-from cardTags, tags
-where cardTags.tagId = tags.id
-%s
-group by cardTags.cardId""" % limit)
- if dirty:
- extra = ", modified = :m "
- else:
- extra = ""
- for pri in range(5):
- cs = [c[0] for c in cards if c[1] == pri]
- if cs:
- # catch review early & buried but not suspended
- self.s.statement((
- "update cards set priority = :pri %s where id in %s "
- "and priority != :pri and priority >= -2") % (
- extra, ids2str(cs)), pri=pri, m=time.time())
-
- def updatePriority(self, card):
- "Update priority on a single card."
- self.s.flush()
- self.updatePriorities([card.id])
-
# Suspending
##########################################################################
# when older clients are upgraded, we can remove the code which touches
- # priorities & isDue
+ # priorities
def suspendCards(self, ids):
"Suspend cards. Caller must .reset()"
@@ -1245,7 +245,7 @@
self.s.statement("""
update cards
set type = relativeDelay - 3,
-priority = -3, modified = :t, isDue=0
+priority = -3, modified = :t
where type >= 0 and id in %s""" % ids2str(ids), t=time.time())
self.flushMod()
self.finishProgress()
@@ -1257,7 +257,6 @@
update cards set type = relativeDelay, priority=0, modified=:t
where type < 0 and id in %s""" %
ids2str(ids), t=time.time())
- self.updatePriorities(ids)
self.flushMod()
self.finishProgress()
@@ -1267,18 +266,11 @@
if card.type in (0,1,2):
card.priority = -2
card.type += 3
- card.isDue = 0
self.flushMod()
# Counts
##########################################################################
- def hiddenCards(self):
- "Assumes queue finished. True if some due cards have not been shown."
- return self.s.scalar("""
-select 1 from cards where combinedDue < :now
-and type between 0 and 1 limit 1""", now=self.dueCutoff)
-
def newCardsDoneToday(self):
return (self._dailyStats.newEase0 +
self._dailyStats.newEase1 +
@@ -1286,73 +278,43 @@
self._dailyStats.newEase3 +
self._dailyStats.newEase4)
- def spacedCardCount(self):
- "Number of spaced cards."
- return self.s.scalar("""
-select count(cards.id) from cards where
-combinedDue > :now and due < :now""", now=time.time())
-
def isEmpty(self):
- return not self.cardCount
+ return not self.s.query(Card).first()
def matureCardCount(self):
- return self.s.scalar(
- "select count(id) from cards where interval >= :t ",
- t=MATURE_THRESHOLD)
+ return self.s.query(Card).filter(Card.interval >= MATURE_THRESHOLD).count()
def youngCardCount(self):
- return self.s.scalar(
- "select count(id) from cards where interval < :t "
- "and reps != 0", t=MATURE_THRESHOLD)
+ return self.s.query(Card).filter(Card.interval < MATURE_THRESHOLD).filter(
+ Card.reps != 0).count()
- def newCountAll(self):
+ def newCount(self):
"All new cards, including spaced."
- return self.s.scalar(
- "select count(id) from cards where relativeDelay = 2")
+ return self.s.query(Card).filter_by(relativeDelay = 2).count()
- def seenCardCount(self):
- return self.s.scalar(
- "select count(id) from cards where relativeDelay between 0 and 1")
+ def cardCount(self):
+ return self.s.query(Card).count()
- # Card predicates
- ##########################################################################
-
- def cardState(self, card):
- if self.cardIsNew(card):
- return "new"
- elif card.interval > MATURE_THRESHOLD:
- return "mature"
- return "young"
-
- def cardIsNew(self, card):
- "True if a card has never been seen before."
- return card.reps == 0
-
- def cardIsBeingLearnt(self, card):
- "True if card should use present intervals."
- return card.lastInterval < 7
-
- def cardIsYoung(self, card):
- "True if card is not new and not mature."
- return (not self.cardIsNew(card) and
- not self.cardIsMature(card))
+ def revCount(self):
+ return self.s.query(Card).filter_by(type=1).filter(Card.combinedDue < time.time()).count()
- def cardIsMature(self, card):
- return card.interval >= MATURE_THRESHOLD
+ def failedCount(self):
+ return self.s.query(Card).filter_by(successive=0). \
+ filter(Card.reps > 0).filter(Card.type.in_((1, 2))).count()
# Stats
##########################################################################
- def getStats(self, short=False):
+ def getStats(self):
"Return some commonly needed stats."
- stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats)
+ stats = anki.stats.getStatistics(self.s)
# add scheduling related stats
- stats['new'] = self.newCountToday
- stats['failed'] = self.failedSoonCount
- stats['rev'] = self.revCount
+ stats['new'] = self.newCount()
+ stats['failed'] = self.failedCount()
+ stats['rev'] = self.revCount()
if stats['dAverageTime']:
stats['timeLeft'] = anki.utils.fmtTimeSpan(
- self.getETA(stats), pad=0, point=1, short=short)
+ self.getETA(stats), pad=0, point=1)
else:
stats['timeLeft'] = _("Unknown")
return stats
@@ -1393,9 +355,8 @@
return None
# proceed
cards = []
- self.s.save(fact)
+ self.s.add(fact)
# update field cache
- self.factCount += 1
self.flushMod()
isRandom = self.newCardOrder == NEW_CARDS_RANDOM
if isRandom:
@@ -1403,7 +364,7 @@
t = time.time()
for cardModel in cms:
created = fact.created + 0.00001*cardModel.ordinal
- card = anki.cards.Card(fact, cardModel, created)
+ card = Card(fact, cardModel, created)
if isRandom:
card.due = due
card.combinedDue = due
@@ -1411,11 +372,6 @@
cards.append(card)
# update card q/a
fact.setModified(True, self)
- self.updateFactTags([fact.id])
- # this will call reset() which will update counts
- self.updatePriorities([c.id for c in cards])
- # keep track of last used tags for convenience
- self.lastTags = fact.tags
self.flushMod()
if reset:
self.reset()
@@ -1466,13 +422,9 @@
where factId = :fid and cardModelId = :cmid""",
fid=fact.id, cmid=cardModel.id) == 0:
# enough for 10 card models assuming 0.00001 timer precision
- card = anki.cards.Card(
+ card = Card(
fact, cardModel,
fact.created+0.0001*cardModel.ordinal)
- self.updateCardTags([card.id])
- self.updatePriority(card)
- self.cardCount += 1
- self.newCount += 1
ids.append(card.id)
if ids:
@@ -1538,7 +490,7 @@
# proceed
cards = []
for cardModel in cms:
- card = anki.cards.Card(fact, cardModel)
+ card = Card(fact, cardModel)
cards.append(card)
fact.setModified(textChanged=True, deck=self, media=False)
return cards
@@ -1610,7 +562,6 @@
if self.s.scalar("select count(id) from models where id=:id",
id=model.id):
# delete facts/cards
- self.currentModel
self.deleteCards(self.s.column0("""
select cards.id from cards, facts where
facts.modelId = :id and
@@ -1638,51 +589,6 @@
if not self.modelUseCount(model):
self.deleteModel(model)
- def rebuildCSS(self):
- # css for all fields
- def _genCSS(prefix, row):
- (id, fam, siz, col, align, rtl, pre) = row
- t = ""
- if fam: t += 'font-family:"%s";' % toPlatformFont(fam)
- if siz: t += 'font-size:%dpx;' % siz
- if col: t += 'color:%s;' % col
- if rtl == "rtl":
- t += "direction:rtl;unicode-bidi:embed;"
- if pre:
- t += "white-space:pre-wrap;"
- 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,
- features, editFontFamily from fieldModels""")])
- cardRows = self.s.all("""
-select id, null, null, null, questionAlign, 0, 0 from cardModels""")
- css += "".join([_genCSS("#cmq", row) for row in cardRows])
- css += "".join([_genCSS("#cma", row) for row in cardRows])
- css += "".join([".cmb%s {background:%s;}\n" %
- (hexifyID(row[0]), row[1]) for row in self.s.all("""
-select id, lastFontColour from cardModels""")])
- self.css = css
- self.setVar("cssCache", css, mod=False)
- self.addHexCache()
- return css
-
- def addHexCache(self):
- ids = self.s.column0("""
-select id from fieldModels union
-select id from cardModels union
-select id from models""")
- cache = {}
- for id in ids:
- cache[id] = hexifyID(id)
- self.setVar("hexCache", simplejson.dumps(cache), mod=False)
-
def copyModel(self, oldModel):
"Add a new model to DB based on MODEL."
m = Model(_("%s copy") % oldModel.name)
@@ -1770,15 +676,12 @@
ordinal = :ord
where id in %s""" % ids2str(ids), new=new.id, ord=new.ordinal)
self.updateProgress()
- self.updateCardQACacheFromIds(factIds, type="facts")
self.flushMod()
self.updateProgress()
cardIds = self.s.column0(
"select id from cards where factId in %s" %
ids2str(factIds))
- self.updateCardTags(cardIds)
self.updateProgress()
- self.updatePriorities(cardIds)
self.updateProgress()
self.refreshSession()
self.finishProgress()
@@ -1807,7 +710,6 @@
for t in types:
for fmt in ('qformat', 'aformat'):
setattr(cm, fmt, getattr(cm, fmt).replace(t, ""))
- self.updateCardsFromModel(model)
model.setModified()
self.flushMod()
self.finishProgress()
@@ -1888,127 +790,6 @@
model.setModified()
self.flushMod()
- def updateCardsFromModel(self, model, dirty=True):
- "Update all card question/answer when model changes."
- ids = self.s.all("""
-select cards.id, cards.cardModelId, cards.factId, facts.modelId from
-cards, facts where
-cards.factId = facts.id and
-facts.modelId = :id""", id=model.id)
- if not ids:
- return
- self.updateCardQACache(ids, dirty)
-
- def updateCardsFromFactIds(self, ids, dirty=True):
- "Update all card question/answer when model changes."
- ids = self.s.all("""
-select cards.id, cards.cardModelId, cards.factId, facts.modelId from
-cards, facts where
-cards.factId = facts.id and
-facts.id in %s""" % ids2str(ids))
- if not ids:
- return
- self.updateCardQACache(ids, dirty)
-
- def updateCardQACacheFromIds(self, ids, type="cards"):
- "Given a list of card or fact ids, update q/a cache."
- if type == "facts":
- # convert to card ids
- ids = self.s.column0(
- "select id from cards where factId in %s" % ids2str(ids))
- rows = self.s.all("""
-select c.id, c.cardModelId, f.id, f.modelId
-from cards as c, facts as f
-where c.factId = f.id
-and c.id in %s""" % ids2str(ids))
- self.updateCardQACache(rows)
-
- def updateCardQACache(self, ids, dirty=True):
- "Given a list of (cardId, cardModelId, factId, modId), update q/a cache."
- if dirty:
- mod = ", modified = %f" % time.time()
- else:
- mod = ""
- # tags
- cids = ids2str([x[0] for x in ids])
- tags = dict([(x[0], x[1:]) for x in
- self.splitTagsList(
- where="and cards.id in %s" % cids)])
- 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], self)
- for (cid, cmid, fid, mid) in ids]
- if pend:
- # find existing media references
- files = {}
- for txt in self.s.column0(
- "select question || answer from cards where id in %s" %
- cids):
- for f in mediaFiles(txt):
- if f in files:
- files[f] -= 1
- else:
- files[f] = -1
- # determine ref count delta
- for p in pend:
- for type in ("question", "answer"):
- txt = p[type]
- for f in mediaFiles(txt):
- if f in files:
- files[f] += 1
- else:
- files[f] = 1
- # update references - this could be more efficient
- for (f, cnt) in files.items():
- if not cnt:
- continue
- updateMediaCount(self, f, cnt)
- # update q/a
- self.s.execute("""
- update cards set
- question = :question, answer = :answer
- %s
- where id = :id""" % mod, pend)
- # update fields cache
- self.updateFieldCache(facts.keys())
- if dirty:
- self.flushMod()
-
- def updateFieldCache(self, fids):
- "Add stripped HTML cache for sorting/searching."
- try:
- all = self.s.all(
- ("select factId, group_concat(value, ' ') from fields "
- "where factId in %s group by factId") % ids2str(fids))
- except:
- # older sqlite doesn't support group_concat. this code taken from
- # the wm port
- all=[]
- for factId in fids:
- values=self.s.all("select value from fields where value is not NULL and factId=%(factId)i" % {"factId": factId})
- value_list=[]
- for row in values:
- value_list.append(row[0])
- concatenated_values=' '.join(value_list)
- all.append([factId, concatenated_values])
- r = []
- from anki.utils import stripHTMLMedia
- for a in all:
- r.append({'id':a[0], 'v':stripHTMLMedia(a[1])})
- self.s.statements(
- "update facts set spaceUntil=:v where id=:id", r)
-
def rebuildCardOrdinals(self, ids):
"Update all card models in IDS. Caller must update model modtime."
self.s.flush()
@@ -2024,7 +805,6 @@
self.s.statement("""
update cards set cardModelId = :newId
where id in %s""" % ids2str(cardIds), newId=newCardModelId)
- self.updateCardQACacheFromIds(cardIds)
self.flushMod()
# Tags: querying
@@ -2115,56 +895,6 @@
# Tags: caching
##########################################################################
- def updateFactTags(self, factIds):
- self.updateCardTags(self.s.column0(
- "select id from cards where factId in %s" %
- ids2str(factIds)))
-
- def updateModelTags(self, modelId):
- self.updateCardTags(self.s.column0("""
-select cards.id from cards, facts where
-cards.factId = facts.id and
-facts.modelId = :id""", id=modelId))
-
- def updateCardTags(self, cardIds=None):
- self.s.flush()
- if cardIds is None:
- self.s.statement("delete from cardTags")
- self.s.statement("delete from tags")
- tids = tagIds(self.s, self.allTags_())
- rows = self.splitTagsList()
- else:
- self.s.statement("delete from cardTags where cardId in %s" %
- ids2str(cardIds))
- fids = ids2str(self.s.column0(
- "select factId from cards where id in %s" %
- ids2str(cardIds)))
- tids = tagIds(self.s, self.allTags_(
- where="where id in %s" % fids))
- rows = self.splitTagsList(
- where="and facts.id in %s" % fids)
- d = []
- for (id, fact, model, templ) in rows:
- for tag in parseTags(fact):
- d.append({"cardId": id,
- "tagId": tids[tag.lower()],
- "src": 0})
- for tag in parseTags(model):
- d.append({"cardId": id,
- "tagId": tids[tag.lower()],
- "src": 1})
- for tag in parseTags(templ):
- d.append({"cardId": id,
- "tagId": tids[tag.lower()],
- "src": 2})
- if d:
- self.s.statements("""
-insert into cardTags
-(cardId, tagId, src) values
-(:cardId, :tagId, :src)""", d)
- self.s.execute(
- "delete from tags where priority = 2 and id not in "+
- "(select distinct tagId from cardTags)")
def updateTagsForModel(self, model):
cards = self.s.all("""
@@ -2230,9 +960,6 @@
cardIds = self.s.column0(
"select id from cards where factId in %s" %
ids2str(factIds))
- self.updateCardQACacheFromIds(factIds, type="facts")
- self.updateCardTags(cardIds)
- self.updatePriorities(cardIds)
self.flushMod()
self.finishProgress()
self.refreshSession()
@@ -2264,9 +991,6 @@
cardIds = self.s.column0(
"select id from cards where factId in %s" %
ids2str(factIds))
- self.updateCardQACacheFromIds(factIds, type="facts")
- self.updateCardTags(cardIds)
- self.updatePriorities(cardIds)
self.flushMod()
self.finishProgress()
self.refreshSession()
@@ -2676,10 +1400,7 @@
n = 0
qquery += "select id from cards where type = %d" % n
elif token == "delayed":
- qquery += ("select id from cards where "
- "due < %d and combinedDue > %d and "
- "type in (0,1,2)") % (
- self.dueCutoff, self.dueCutoff)
+ qquery += "select id from cards where 0"
elif token == "suspended":
qquery += ("select id from cards where "
"priority = -3")
@@ -2689,7 +1410,7 @@
"from deckvars where key = 'leechFails')")
else: # due
qquery += ("select id from cards where "
- "type in (0,1) and combinedDue < %d") % self.dueCutoff
+ "type in (0,1) and combinedDue < %d") % time.time()
elif type == SEARCH_FID:
if fidquery:
if isNeg:
@@ -2829,8 +1550,6 @@
# update
self.s.statements(
'update fields set value = :val where id = :id', modded)
- self.updateCardQACacheFromIds([f['fid'] for f in modded],
- type="facts")
return len(set([f['fid'] for f in modded]))
# Find duplicates
@@ -2854,14 +1573,17 @@
##########################################################################
def startProgress(self, max=0, min=0, title=None):
+ return
self.enableProgressHandler()
runHook("startProgress", max, min, title)
self.s.flush()
def updateProgress(self, label=None, value=None):
+ return
runHook("updateProgress", label, value)
def finishProgress(self):
+ return
runHook("updateProgress")
runHook("finishProgress")
self.disableProgressHandler()
@@ -2900,29 +1622,6 @@
assert '\\' not in n
return n
- # Session handling
- ##########################################################################
-
- def startSession(self):
- self.lastSessionStart = self.sessionStartTime
- self.sessionStartTime = time.time()
- self.sessionStartReps = self.getStats()['dTotal']
-
- def stopSession(self):
- self.sessionStartTime = 0
-
- def sessionLimitReached(self):
- if not self.sessionStartTime:
- # not started
- return False
- if (self.sessionTimeLimit and time.time() >
- (self.sessionStartTime + self.sessionTimeLimit)):
- return True
- if (self.sessionRepLimit and self.sessionRepLimit <=
- self.getStats()['dTotal'] - self.sessionStartReps):
- return True
- return False
-
# Meta vars
##########################################################################
@@ -3020,6 +1719,9 @@
# Media
##########################################################################
+ def mediaQUrl(self):
+ return PyQt4.QtCore.QUrl.fromLocalFile(self.mediaDir(create=None) + os.sep)
+
def mediaDir(self, create=False):
"Return the media directory if exists. None if couldn't create."
if self.path:
@@ -3079,7 +1781,7 @@
def close(self):
if self.s:
self.s.rollback()
- self.s.clear()
+ self.s.expunge_all()
self.s.close()
self.engine.dispose()
runHook("deckClosed")
@@ -3087,8 +1789,8 @@
def rollback(self):
"Roll back the current transaction and reset session state."
self.s.rollback()
- self.s.clear()
- self.s.update(self)
+ self.s.expunge_all()
+ self.s.add(self)
self.s.refresh(self)
def refreshSession(self):
@@ -3096,24 +1798,6 @@
self.s.flush()
self.s.expire_all()
- def openSession(self):
- "Open a new session. Assumes old session is already closed."
- self.s = SessionHelper(self.Session(), lock=self.needLock)
- self.s.update(self)
- self.refreshSession()
-
- def closeSession(self):
- "Close the current session, saving any changes. Do nothing if no session."
- if self.s:
- self.save()
- try:
- self.s.expunge(self)
- except:
- import sys
- sys.stderr.write("ERROR expunging deck..\n")
- self.s.close()
- self.s = None
-
def setModified(self, newTime=None):
#import traceback; traceback.print_stack()
self.modified = newTime or time.time()
@@ -3247,10 +1931,6 @@
self.finishProgress()
return _("Database file is damaged.\n"
"Please restore from automatic backup (see FAQ).")
- # ensure correct views and indexes are available
- self.updateProgress()
- DeckStorage._addViews(self)
- DeckStorage._addIndices(self)
# does the user have a model?
self.updateProgress(_("Checking schema..."))
if not self.s.scalar("select count(id) from models"):
@@ -3335,13 +2015,6 @@
self.s.statement(
"update cardModels set allowEmptyAnswer = 1, typeAnswer = '' "
"where allowEmptyAnswer is null or typeAnswer is null")
- # fix tags
- self.updateProgress(_("Rebuilding tag cache..."))
- self.updateCardTags()
- # fix any priorities
- self.updateProgress(_("Updating priorities..."))
- self.updateAllPriorities(dirty=False)
- # make sure
self.updateProgress(_("Updating ordinals..."))
self.s.statement("""
update fields set ordinal = (select ordinal from fieldModels
@@ -3355,9 +2028,6 @@
self.s.statements(
"update fields set value=:value where id=:id",
newFields)
- # regenerate question/answer cache
- for m in self.models:
- self.updateCardsFromModel(m, dirty=False)
# force a full sync
self.s.flush()
self.s.statement("update cards set modified = :t", t=time.time())
@@ -3366,7 +2036,6 @@
self.lastSync = 0
# rebuild
self.updateProgress(_("Rebuilding types..."))
- self.rebuildTypes()
# update deck and save
if not quick:
self.flushMod()
@@ -3540,54 +2209,6 @@
self.refreshSession()
runHook("postUndoRedo")
- # Dynamic indices
- ##########################################################################
-
- def updateDynamicIndices(self):
- indices = {
- 'intervalDesc':
- '(type, priority desc, interval desc, factId, combinedDue)',
- 'intervalAsc':
- '(type, priority desc, interval, factId, combinedDue)',
- 'randomOrder':
- '(type, priority desc, factId, ordinal, combinedDue)',
- 'dueAsc':
- '(type, priority desc, due, factId, combinedDue)',
- 'dueDesc':
- '(type, priority desc, due desc, factId, combinedDue)',
- }
- # determine required
- required = []
- if self.revCardOrder == REV_CARDS_OLD_FIRST:
- required.append("intervalDesc")
- if self.revCardOrder == REV_CARDS_NEW_FIRST:
- required.append("intervalAsc")
- if self.revCardOrder == REV_CARDS_RANDOM:
- required.append("randomOrder")
- if (self.revCardOrder == REV_CARDS_DUE_FIRST or
- self.newCardOrder == NEW_CARDS_OLD_FIRST or
- self.newCardOrder == NEW_CARDS_RANDOM):
- required.append("dueAsc")
- if (self.newCardOrder == NEW_CARDS_NEW_FIRST):
- required.append("dueDesc")
- # add/delete
- analyze = False
- for (k, v) in indices.items():
- n = "ix_cards_%s2" % k
- if k in required:
- if not self.s.scalar(
- "select 1 from sqlite_master where name = :n", n=n):
- self.s.statement(
- "create index %s on cards %s" %
- (n, v))
- analyze = True
- else:
- # leave old indices for older clients
- #self.s.statement("drop index if exists ix_cards_%s" % k)
- self.s.statement("drop index if exists %s" % n)
- if analyze:
- self.s.statement("analyze")
-
# Shared decks
##########################################################################
@@ -3608,10 +2229,8 @@
'currentModel': relation(anki.models.Model, primaryjoin=
decksTable.c.currentModelId ==
anki.models.modelsTable.c.id),
- 'models': relation(anki.models.Model, post_update=True,
- primaryjoin=
- decksTable.c.id ==
- anki.models.modelsTable.c.deckId),
+ 'models': relation(anki.models.Model, post_update=True, backref='deck',
+ primaryjoin= decksTable.c.id == anki.models.modelsTable.c.deckId),
})
# Deck storage
@@ -3622,48 +2241,30 @@
class DeckStorage(object):
- def Deck(path=None, backup=True, lock=True, pool=True, rebuild=True):
+ @staticmethod
+ def Deck(path=None, backup=True):
"Create a new deck or attach to an existing one."
create = True
if path is None:
- sqlpath = None
+ sqlpath = "sqlite://"
else:
path = os.path.abspath(path)
- # check if we need to init
if os.path.exists(path):
create = False
# sqlite needs utf8
- sqlpath = path.encode("utf-8")
+ sqlpath = "sqlite:///" + path.encode("utf-8")
try:
- (engine, session) = DeckStorage._attach(sqlpath, create, pool)
+ engine = create_engine(sqlpath, connect_args={'timeout': 0},
+ strategy="threadlocal")
+ session = sessionmaker(bind=engine, expire_on_commit=False)
s = session()
if create:
- ver = 999
metadata.create_all(engine)
- deck = DeckStorage._init(s)
+ deck = Deck()
+ deck.id = 1
+ s.add(deck)
+ s.flush()
else:
- ver = s.scalar("select version from decks limit 1")
- if ver < 19:
- for st in (
- "decks add column newCardsPerDay integer not null default 20",
- "decks add column sessionRepLimit integer not null default 100",
- "decks add column sessionTimeLimit integer not null default 1800",
- "decks add column utcOffset numeric(10, 2) not null default 0",
- "decks add column cardCount integer not null default 0",
- "decks add column factCount integer not null default 0",
- "decks add column failedNowCount integer not null default 0",
- "decks add column failedSoonCount integer not null default 0",
- "decks add column revCount integer not null default 0",
- "decks add column newCount integer not null default 0",
- "decks add column revCardOrder integer not null default 0",
- "cardModels add column allowEmptyAnswer boolean not null default 1",
- "cardModels add column typeAnswer text not null default ''"):
- try:
- s.execute("alter table " + st)
- except:
- pass
- if ver < DECK_VERSION:
- metadata.create_all(engine)
deck = s.query(Deck).get(1)
if not deck:
raise DeckAccessError(_("Deck missing core table"),
@@ -3672,51 +2273,22 @@
deck.path = path
deck.engine = engine
deck.Session = session
- deck.needLock = lock
- deck.progressHandlerCalled = 0
- deck.progressHandlerEnabled = False
- if pool:
- try:
- deck.engine.raw_connection().set_progress_handler(
- deck.progressHandler, 100)
- except:
- print "please install pysqlite 2.4 for better progress dialogs"
deck.engine.execute("pragma locking_mode = exclusive")
- deck.s = SessionHelper(s, lock=lock)
- # force a write lock
- deck.s.execute("update decks set modified = modified")
- needUnpack = False
+ deck.s = SessionHelper(s)
if deck.utcOffset in (-1, -2):
- # do the rest later
- needUnpack = deck.utcOffset == -1
- # make sure we do this before initVars
- DeckStorage._setUTCOffset(deck)
+ # 4am
+ deck.utcOffset = time.timezone + 60*60*4
deck.created = time.time()
- if ver < 27:
- initTagTables(deck.s)
if create:
# new-style file format
deck.s.commit()
deck.s.execute("pragma legacy_file_format = off")
deck.s.execute("pragma default_cache_size= 20000")
deck.s.execute("vacuum")
- # add views/indices
initTagTables(deck.s)
- DeckStorage._addViews(deck)
- DeckStorage._addIndices(deck)
deck.s.statement("analyze")
- deck._initVars()
- deck.updateTagPriorities()
- else:
- if backup:
+ elif backup:
DeckStorage.backup(deck, path)
- deck._initVars()
- try:
- deck = DeckStorage._upgradeDeck(deck, path)
- except:
- traceback.print_exc()
- deck.fixIntegrity()
- deck = DeckStorage._upgradeDeck(deck, path)
except OperationalError, e:
engine.dispose()
if (str(e.orig).startswith("database table is locked") or
@@ -3725,712 +2297,18 @@
type="inuse")
else:
raise e
- if not rebuild:
- # minimal startup
- deck._globalStats = globalStats(deck)
- deck._dailyStats = dailyStats(deck)
- return deck
- if needUnpack:
- deck.startProgress()
- DeckStorage._addIndices(deck)
- for m in deck.models:
- deck.updateCardsFromModel(m)
- deck.finishProgress()
- oldMod = deck.modified
+ deck.tmpMediaDir = None
+ deck.mediaPrefix = ""
+ deck.lastLoaded = time.time()
+ deck.undoEnabled = False
+ deck._scheduler = anki.schedulers.StandardScheduler(deck)
# fix a bug with current model being unset
if not deck.currentModel and deck.models:
deck.currentModel = deck.models[0]
- # ensure the necessary indices are available
- deck.updateDynamicIndices()
# FIXME: temporary code for upgrade
- # - ensure cards suspended on older clients are recognized
- deck.s.statement("""
-update cards set type = type - 3 where type between 0 and 2 and priority = -3""")
- # - new delay1 handling
- if deck.delay1 > 7:
- deck.delay1 = 0
- # unsuspend buried/rev early - can remove priorities in the future
- ids = deck.s.column0(
- "select id from cards where type > 2 or priority between -2 and -1")
- if ids:
- deck.updatePriorities(ids)
- deck.s.statement(
- "update cards set type = relativeDelay where type > 2")
- deck.s.commit()
# check if deck has been moved, and disable syncing
deck.checkSyncHash()
- # determine starting factor for new cards
- deck.averageFactor = (deck.s.scalar(
- "select avg(factor) from cards where type = 1")
- or Deck.initialFactor)
- deck.averageFactor = max(deck.averageFactor, Deck.minimumAverage)
- # rebuild queue
- deck.reset()
- # make sure we haven't accidentally bumped the modification time
- assert deck.modified == oldMod
- return deck
- Deck = staticmethod(Deck)
-
- def _attach(path, create, pool=True):
- "Attach to a file, initializing DB"
- if path is None:
- path = "sqlite://"
- else:
- path = "sqlite:///" + path
- if pool:
- # open and lock connection for single use
- engine = create_engine(path, connect_args={'timeout': 0},
- strategy="threadlocal")
- else:
- # no pool & concurrent access w/ timeout
- engine = create_engine(path,
- poolclass=NullPool,
- connect_args={'timeout': 60})
- session = sessionmaker(bind=engine,
- autoflush=False,
- autocommit=True)
- return (engine, session)
- _attach = staticmethod(_attach)
-
- def _init(s):
- "Add a new deck to the database. Return saved deck."
- deck = Deck()
- if sqlalchemy.__version__.startswith("0.4."):
- s.save(deck)
- else:
- s.add(deck)
- s.flush()
return deck
- _init = staticmethod(_init)
-
- def _addIndices(deck):
- "Add indices to the DB."
- # counts, failed cards
- deck.s.statement("""
-create index if not exists ix_cards_typeCombined on cards
-(type, combinedDue, factId)""")
- # scheduler-agnostic type
- deck.s.statement("""
-create index if not exists ix_cards_relativeDelay on cards
-(relativeDelay)""")
- # index on modified, to speed up sync summaries
- deck.s.statement("""
-create index if not exists ix_cards_modified on cards
-(modified)""")
- deck.s.statement("""
-create index if not exists ix_facts_modified on facts
-(modified)""")
- # priority - temporary index to make compat code faster. this can be
- # removed when all clients are on 1.2, as can the ones below
- deck.s.statement("""
-create index if not exists ix_cards_priority on cards
-(priority)""")
- # average factor
- deck.s.statement("""
-create index if not exists ix_cards_factor on cards
-(type, factor)""")
- # card spacing
- deck.s.statement("""
-create index if not exists ix_cards_factId on cards (factId)""")
- # stats
- deck.s.statement("""
-create index if not exists ix_stats_typeDay on stats (type, day)""")
- # fields
- deck.s.statement("""
-create index if not exists ix_fields_factId on fields (factId)""")
- deck.s.statement("""
-create index if not exists ix_fields_fieldModelId on fields (fieldModelId)""")
- deck.s.statement("""
-create index if not exists ix_fields_value on fields (value)""")
- # media
- deck.s.statement("""
-create unique index if not exists ix_media_filename on media (filename)""")
- deck.s.statement("""
-create index if not exists ix_media_originalPath on media (originalPath)""")
- # deletion tracking
- deck.s.statement("""
-create index if not exists ix_cardsDeleted_cardId on cardsDeleted (cardId)""")
- deck.s.statement("""
-create index if not exists ix_modelsDeleted_modelId on modelsDeleted (modelId)""")
- deck.s.statement("""
-create index if not exists ix_factsDeleted_factId on factsDeleted (factId)""")
- deck.s.statement("""
-create index if not exists ix_mediaDeleted_factId on mediaDeleted (mediaId)""")
- # tags
- txt = "create unique index if not exists ix_tags_tag on tags (tag)"
- try:
- deck.s.statement(txt)
- except:
- deck.s.statement("""
-delete from tags where exists (select 1 from tags t2 where tags.tag = t2.tag
-and tags.rowid > t2.rowid)""")
- deck.s.statement(txt)
- deck.s.statement("""
-create index if not exists ix_cardTags_tagCard on cardTags (tagId, cardId)""")
- deck.s.statement("""
-create index if not exists ix_cardTags_cardId on cardTags (cardId)""")
- _addIndices = staticmethod(_addIndices)
-
- def _addViews(deck):
- "Add latest version of SQL views to DB."
- s = deck.s
- # old views
- s.statement("drop view if exists failedCards")
- s.statement("drop view if exists revCardsOld")
- s.statement("drop view if exists revCardsNew")
- s.statement("drop view if exists revCardsDue")
- s.statement("drop view if exists revCardsRandom")
- s.statement("drop view if exists acqCardsRandom")
- s.statement("drop view if exists acqCardsOld")
- s.statement("drop view if exists acqCardsNew")
- # failed cards
- s.statement("""
-create view failedCards as
-select * from cards
-where type = 0 and isDue = 1
-order by type, isDue, combinedDue
-""")
- # rev cards
- s.statement("""
-create view revCardsOld as
-select * from cards
-where type = 1 and isDue = 1
-order by priority desc, interval desc""")
- s.statement("""
-create view revCardsNew as
-select * from cards
-where type = 1 and isDue = 1
-order by priority desc, interval""")
- s.statement("""
-create view revCardsDue as
-select * from cards
-where type = 1 and isDue = 1
-order by priority desc, due""")
- s.statement("""
-create view revCardsRandom as
-select * from cards
-where type = 1 and isDue = 1
-order by priority desc, factId, ordinal""")
- # new cards
- s.statement("""
-create view acqCardsOld as
-select * from cards
-where type = 2 and isDue = 1
-order by priority desc, due""")
- s.statement("""
-create view acqCardsNew as
-select * from cards
-where type = 2 and isDue = 1
-order by priority desc, due desc""")
- _addViews = staticmethod(_addViews)
-
- def _upgradeDeck(deck, path):
- "Upgrade deck to the latest version."
- if deck.version < DECK_VERSION:
- prog = True
- deck.startProgress()
- deck.updateProgress(_("Upgrading Deck..."))
- if deck.utcOffset == -1:
- # we're opening a shared deck with no indices - we'll need
- # them if we want to rebuild the queue
- DeckStorage._addIndices(deck)
- oldmod = deck.modified
- else:
- prog = False
- deck.path = path
- if deck.version == 0:
- # new columns
- try:
- deck.s.statement("""
- alter table cards add column spaceUntil float not null default 0""")
- deck.s.statement("""
- alter table cards add column relativeDelay float not null default 0.0""")
- deck.s.statement("""
- alter table cards add column isDue boolean not null default 0""")
- deck.s.statement("""
- alter table cards add column type integer not null default 0""")
- deck.s.statement("""
- alter table cards add column combinedDue float not null default 0""")
- # update cards.spaceUntil based on old facts
- deck.s.statement("""
- update cards
- set spaceUntil = (select (case
- when cards.id = facts.lastCardId
- then 0
- else facts.spaceUntil
- end) from cards as c, facts
- where c.factId = facts.id
- and cards.id = c.id)""")
- deck.s.statement("""
- update cards
- set combinedDue = max(due, spaceUntil)
- """)
- except:
- print "failed to upgrade"
- # rebuild with new file format
- deck.s.commit()
- deck.s.execute("pragma legacy_file_format = off")
- deck.s.execute("vacuum")
- # add views/indices
- DeckStorage._addViews(deck)
- DeckStorage._addIndices(deck)
- # rebuild type and delay cache
- deck.rebuildTypes()
- deck.reset()
- # bump version
- deck.version = 1
- # optimize indices
- deck.s.statement("analyze")
- if deck.version == 1:
- # fix indexes and views
- deck.s.statement("drop index if exists ix_cards_newRandomOrder")
- deck.s.statement("drop index if exists ix_cards_newOrderedOrder")
- DeckStorage._addViews(deck)
- DeckStorage._addIndices(deck)
- deck.rebuildTypes()
- # optimize indices
- deck.s.statement("analyze")
- deck.version = 2
- if deck.version == 2:
- # compensate for bug in 0.9.7 by rebuilding isDue and priorities
- deck.s.statement("update cards set isDue = 0")
- deck.updateAllPriorities(dirty=False)
- # compensate for bug in early 0.9.x where fieldId was not unique
- deck.s.statement("update fields set id = random()")
- deck.version = 3
- if deck.version == 3:
- # remove conflicting and unused indexes
- deck.s.statement("drop index if exists ix_cards_isDueCombined")
- deck.s.statement("drop index if exists ix_facts_lastCardId")
- deck.s.statement("drop index if exists ix_cards_successive")
- deck.s.statement("drop index if exists ix_cards_priority")
- deck.s.statement("drop index if exists ix_cards_reps")
- deck.s.statement("drop index if exists ix_cards_due")
- deck.s.statement("drop index if exists ix_stats_type")
- deck.s.statement("drop index if exists ix_stats_day")
- deck.s.statement("drop index if exists ix_factsDeleted_cardId")
- deck.s.statement("drop index if exists ix_modelsDeleted_cardId")
- DeckStorage._addIndices(deck)
- deck.s.statement("analyze")
- deck.version = 4
- if deck.version == 4:
- # decks field upgraded earlier
- deck.version = 5
- if deck.version == 5:
- # new spacing
- deck.newCardSpacing = NEW_CARDS_DISTRIBUTE
- deck.version = 6
- # low priority cards now stay in same queue
- deck.rebuildTypes()
- if deck.version == 6:
- # removed 'new cards first' option, so order has changed
- deck.newCardSpacing = NEW_CARDS_DISTRIBUTE
- deck.version = 7
- # 8 upgrade code removed as obsolete>
- if deck.version < 9:
- # back up the media dir again, just in case
- shutil.copytree(deck.mediaDir(create=True),
- deck.mediaDir() + "-old-%s" %
- hash(time.time()))
- # backup media
- media = deck.s.all("""
-select filename, size, created, originalPath, description from media""")
- # fix mediaDeleted definition
- deck.s.execute("drop table mediaDeleted")
- deck.s.execute("drop table media")
- metadata.create_all(deck.engine)
- # restore
- h = []
- for row in media:
- h.append({
- 'id': genID(),
- 'filename': row[0],
- 'size': row[1],
- 'created': row[2],
- 'originalPath': row[3],
- 'description': row[4]})
- if h:
- deck.s.statements("""
-insert into media values (
-:id, :filename, :size, :created, :originalPath, :description)""", h)
- deck.version = 9
- if deck.version < 10:
- deck.s.statement("""
-alter table models add column source integer not null default 0""")
- deck.version = 10
- if deck.version < 11:
- DeckStorage._setUTCOffset(deck)
- deck.version = 11
- deck.s.commit()
- if 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.s.statement("analyze")
- if deck.version < 13:
- deck.reset()
- deck.rebuildCounts()
- # regenerate question/answer cache
- for m in deck.models:
- deck.updateCardsFromModel(m, dirty=False)
- deck.version = 13
- if deck.version < 14:
- deck.s.statement("""
-update cards set interval = 0
-where interval < 1""")
- deck.version = 14
- if deck.version < 15:
- deck.delay1 = deck.delay0
- deck.delay2 = 0.0
- deck.version = 15
- if deck.version < 16:
- deck.version = 16
- if deck.version < 17:
- deck.s.statement("drop view if exists acqCards")
- deck.s.statement("drop view if exists futureCards")
- deck.s.statement("drop view if exists revCards")
- deck.s.statement("drop view if exists typedCards")
- deck.s.statement("drop view if exists failedCardsNow")
- deck.s.statement("drop view if exists failedCardsSoon")
- 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_combinedDue")
- # add new views
- DeckStorage._addViews(deck)
- DeckStorage._addIndices(deck)
- deck.version = 17
- if deck.version < 18:
- deck.s.statement(
- "create table undoLog (seq integer primary key, sql text)")
- deck.version = 18
- deck.s.commit()
- DeckStorage._addIndices(deck)
- deck.s.statement("analyze")
- if deck.version < 19:
- # permanent undo log causes various problems, revert to temp
- deck.s.statement("drop table undoLog")
- deck.sessionTimeLimit = 600
- deck.sessionRepLimit = 0
- deck.version = 19
- deck.s.commit()
- if deck.version < 20:
- DeckStorage._addViews(deck)
- DeckStorage._addIndices(deck)
- deck.version = 20
- deck.s.commit()
- if deck.version < 21:
- deck.s.statement("vacuum")
- deck.s.statement("analyze")
- deck.version = 21
- deck.s.commit()
- if deck.version < 22:
- deck.s.statement(
- 'update cardModels set typeAnswer = ""')
- deck.version = 22
- deck.s.commit()
- if deck.version < 23:
- try:
- deck.s.execute("drop table undoLog")
- except:
- pass
- deck.version = 23
- deck.s.commit()
- if deck.version < 24:
- deck.s.statement(
- "update cardModels set lastFontColour = '#ffffff'")
- deck.version = 24
- deck.s.commit()
- if deck.version < 25:
- deck.s.statement("drop index if exists ix_cards_priorityDue")
- deck.s.statement("drop index if exists ix_cards_priorityDueReal")
- DeckStorage._addViews(deck)
- DeckStorage._addIndices(deck)
- deck.updateDynamicIndices()
- deck.version = 25
- deck.s.commit()
- if deck.version < 26:
- # no spaces in tags anymore, as separated by space
- def munge(tags):
- tags = re.sub(", ?", "--tmp--", tags)
- tags = re.sub(" - ", "-", tags)
- tags = re.sub(" ", "-", tags)
- tags = re.sub("--tmp--", " ", tags)
- tags = canonifyTags(tags)
- return tags
- rows = deck.s.all('select id, tags from facts')
- d = []
- for (id, tags) in rows:
- d.append({
- 'i': id,
- 't': munge(tags),
- })
- deck.s.statements(
- "update facts set tags = :t where id = :i", d)
- for k in ('highPriority', 'medPriority',
- 'lowPriority', 'suspended'):
- x = getattr(deck, k)
- setattr(deck, k, munge(x))
- for m in deck.models:
- for cm in m.cardModels:
- cm.name = munge(cm.name)
- m.tags = munge(m.tags)
- deck.updateCardsFromModel(m, dirty=False)
- deck.version = 26
- deck.s.commit()
- deck.s.statement("vacuum")
- if deck.version < 27:
- DeckStorage._addIndices(deck)
- deck.updateCardTags()
- deck.updateAllPriorities(dirty=False)
- deck.version = 27
- deck.s.commit()
- if deck.version < 28:
- deck.s.statement("pragma default_cache_size= 20000")
- deck.version = 28
- deck.s.commit()
- if deck.version < 30:
- # remove duplicates from review history
- deck.s.statement("""
-delete from reviewHistory where id not in (
-select min(id) from reviewHistory group by cardId, time);""")
- deck.version = 30
- deck.s.commit()
- if deck.version < 31:
- # recreate review history table
- deck.s.statement("drop index if exists ix_reviewHistory_unique")
- schema = """
-CREATE TABLE %s (
-cardId INTEGER NOT NULL,
-time NUMERIC(10, 2) NOT NULL,
-lastInterval NUMERIC(10, 2) NOT NULL,
-nextInterval NUMERIC(10, 2) NOT NULL,
-ease INTEGER NOT NULL,
-delay NUMERIC(10, 2) NOT NULL,
-lastFactor NUMERIC(10, 2) NOT NULL,
-nextFactor NUMERIC(10, 2) NOT NULL,
-reps NUMERIC(10, 2) NOT NULL,
-thinkingTime NUMERIC(10, 2) NOT NULL,
-yesCount NUMERIC(10, 2) NOT NULL,
-noCount NUMERIC(10, 2) NOT NULL,
-PRIMARY KEY (cardId, time))"""
- deck.s.statement(schema % "revtmp")
- deck.s.statement("""
-insert into revtmp
-select cardId, time, lastInterval, nextInterval, ease, delay, lastFactor,
-nextFactor, reps, thinkingTime, yesCount, noCount from reviewHistory""")
- deck.s.statement("drop table reviewHistory")
- metadata.create_all(deck.engine)
- deck.s.statement(
- "insert into reviewHistory select * from revtmp")
- deck.s.statement("drop table revtmp")
- deck.version = 31
- deck.s.commit()
- deck.s.statement("vacuum")
- if deck.version < 32:
- deck.s.execute("drop index if exists ix_cardTags_tagId")
- deck.s.execute("drop index if exists ix_cardTags_cardId")
- DeckStorage._addIndices(deck)
- deck.s.execute("analyze")
- deck.version = 32
- deck.s.commit()
- if deck.version < 33:
- deck.s.execute("drop index if exists ix_tags_tag")
- DeckStorage._addIndices(deck)
- deck.version = 33
- deck.s.commit()
- if deck.version < 34:
- deck.s.execute("drop view if exists acqCardsRandom")
- deck.s.execute("drop index if exists ix_cards_factId")
- DeckStorage._addIndices(deck)
- deck.updateDynamicIndices()
- deck.version = 34
- deck.s.commit()
- if deck.version < 36:
- deck.s.statement("drop index if exists ix_cards_priorityDue")
- DeckStorage._addIndices(deck)
- deck.s.execute("analyze")
- deck.version = 36
- deck.s.commit()
- if deck.version < 37:
- if deck.getFailedCardPolicy() == 1:
- deck.failedCardMax = 0
- deck.version = 37
- deck.s.commit()
- if deck.version < 39:
- deck.reset()
- # manually suspend all suspended cards
- ids = deck.findCards("tag:suspended")
- if ids:
- # unrolled from suspendCards() to avoid marking dirty
- deck.s.statement(
- "update cards set isDue=0, priority=-3 "
- "where id in %s" % ids2str(ids))
- deck.rebuildCounts()
- # suspended tag obsolete - don't do this yet
- deck.suspended = re.sub(u" ?Suspended ?", u"", deck.suspended)
- deck.updateTagPriorities()
- deck.version = 39
- deck.s.commit()
- if deck.version < 40:
- # now stores media url
- deck.s.statement("update models set features = ''")
- deck.version = 40
- deck.s.commit()
- if deck.version < 43:
- deck.s.statement("update fieldModels set features = ''")
- deck.version = 43
- deck.s.commit()
- if deck.version < 44:
- # leaner indices
- deck.s.statement("drop index if exists ix_cards_factId")
- deck.version = 44
- deck.s.commit()
- if deck.version < 48:
- deck.updateFieldCache(deck.s.column0("select id from facts"))
- deck.version = 48
- deck.s.commit()
- if deck.version < 50:
- # more new type handling
- deck.rebuildTypes()
- deck.version = 50
- deck.s.commit()
- if deck.version < 52:
- dname = deck.name()
- sname = deck.syncName
- if sname and dname != sname:
- deck.notify(_("""\
-When syncing, Anki now uses the same deck name on the server as the deck \
-name on your computer. Because you had '%(dname)s' set to sync to \
-'%(sname)s' on the server, syncing has been temporarily disabled.
-
-If you want to keep your changes to the online version, please use \
-File>Download>Personal Deck to download the online version.
-
-If you want to keep the version on your computer, please enable \
-syncing again via Settings>Deck Properties>Synchronisation.
-
-If you have syncing disabled in the preferences, you can ignore \
-this message. (ERR-0101)""") % {
- 'sname':sname, 'dname':dname})
- deck.disableSyncing()
- elif sname:
- deck.enableSyncing()
- deck.version = 52
- deck.s.commit()
- if deck.version < 53:
- if deck.getBool("perDay"):
- if deck.hardIntervalMin == 0.333:
- deck.hardIntervalMin = max(1.0, deck.hardIntervalMin)
- deck.hardIntervalMax = max(1.1, deck.hardIntervalMax)
- deck.version = 53
- deck.s.commit()
- if deck.version < 54:
- # broken versions of the DB orm die if this is a bool with a
- # non-int value
- deck.s.statement("update fieldModels set editFontFamily = 1");
- deck.version = 54
- deck.s.commit()
- if deck.version < 57:
- deck.version = 57
- deck.s.commit()
- if deck.version < 61:
- # do our best to upgrade templates to the new style
- txt = '''\
-%s'''
- for m in deck.models:
- unstyled = []
- for fm in m.fieldModels:
- # find which fields had explicit formatting
- if fm.quizFontFamily or fm.quizFontSize or fm.quizFontColour:
- pass
- else:
- unstyled.append(fm.name)
- # fill out missing info
- fm.quizFontFamily = fm.quizFontFamily or u"Arial"
- fm.quizFontSize = fm.quizFontSize or 20
- fm.quizFontColour = fm.quizFontColour or "#000000"
- fm.editFontSize = fm.editFontSize or 20
- unstyled = set(unstyled)
- for cm in m.cardModels:
- # embed the old font information into card templates
- cm.qformat = txt % (
- cm.questionFontFamily,
- cm.questionFontSize,
- cm.questionFontColour,
- cm.qformat)
- cm.aformat = txt % (
- cm.answerFontFamily,
- cm.answerFontSize,
- cm.answerFontColour,
- cm.aformat)
- # escape fields that had no previous styling
- for un in unstyled:
- cm.qformat = cm.qformat.replace("%("+un+")s", "{{{%s}}}"%un)
- cm.aformat = cm.aformat.replace("%("+un+")s", "{{{%s}}}"%un)
- # rebuild q/a for the above & because latex has changed
- for m in deck.models:
- deck.updateCardsFromModel(m, dirty=False)
- # rebuild the media db based on new format
- rebuildMediaDir(deck, dirty=False)
- deck.version = 61
- deck.s.commit()
- if deck.version < 62:
- # updated indices
- for d in ("intervalDesc", "intervalAsc", "randomOrder",
- "dueAsc", "dueDesc"):
- deck.s.statement("drop index if exists ix_cards_%s2" % d)
- deck.s.statement("drop index if exists ix_cards_typeCombined")
- DeckStorage._addIndices(deck)
- deck.updateDynamicIndices()
- deck.s.execute("vacuum")
- deck.version = 62
- deck.s.commit()
- if deck.version < 64:
- # remove old static indices, as all clients should be libanki1.2+
- for d in ("ix_cards_duePriority",
- "ix_cards_priorityDue"):
- deck.s.statement("drop index if exists %s" % d)
- # remove old dynamic indices
- for d in ("intervalDesc", "intervalAsc", "randomOrder",
- "dueAsc", "dueDesc"):
- deck.s.statement("drop index if exists ix_cards_%s" % d)
- deck.s.execute("analyze")
- deck.version = 64
- deck.s.commit()
- # note: we keep the priority index for now
- if deck.version < 65:
- # we weren't correctly setting relativeDelay when answering cards
- # in previous versions, so ensure everything is set correctly
- deck.rebuildTypes()
- deck.version = 65
- deck.s.commit()
- # executing a pragma here is very slow on large decks, so we store
- # our own record
- if not deck.getInt("pageSize") == 4096:
- deck.s.commit()
- deck.s.execute("pragma page_size = 4096")
- deck.s.execute("pragma legacy_file_format = 0")
- deck.s.execute("vacuum")
- deck.setVar("pageSize", 4096, mod=False)
- deck.s.commit()
- if prog:
- assert deck.modified == oldmod
- deck.finishProgress()
- return deck
- _upgradeDeck = staticmethod(_upgradeDeck)
-
- def _setUTCOffset(deck):
- # 4am
- deck.utcOffset = time.timezone + 60*60*4
- _setUTCOffset = staticmethod(_setUTCOffset)
def backup(deck, path):
"""Path must not be unicode."""
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/facts.py anki-1.2.8/libanki/anki/facts.py
--- anki-1.2.8.orig/libanki/anki/facts.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/facts.py 2012-04-01 01:59:03.466760810 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -73,6 +74,8 @@
self.fields.append(Field(fm))
self.new = True
+ fieldsDict = association_proxy('fieldsDictFields', 'value')
+
def isNew(self):
return getattr(self, 'new', False)
@@ -83,16 +86,10 @@
return [field.value for field in self.fields]
def __getitem__(self, key):
- try:
- return [f.value for f in self.fields if f.name == key][0]
- except IndexError:
- raise KeyError(key)
+ return self.fieldsDict[key]
def __setitem__(self, key, value):
- try:
- [f for f in self.fields if f.name == key][0].value = value
- except IndexError:
- raise KeyError
+ self.fieldsDict[key] = value
def get(self, key, default):
try:
@@ -144,8 +141,6 @@
assert deck
self.spaceUntil = stripHTMLMedia(u" ".join(
self.values()))
- for card in self.cards:
- card.rebuildQA(deck)
# Fact deletions
##########################################################################
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/graphs.py anki-1.2.8/libanki/anki/graphs.py
--- anki-1.2.8.orig/libanki/anki/graphs.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/graphs.py 2012-03-31 21:04:53.015236943 +0800
@@ -55,13 +55,12 @@
class DeckGraphs(object):
- def __init__(self, deck, width=8, height=3, dpi=75, selective=True):
+ def __init__(self, deck, width=8, height=3, dpi=75):
self.deck = deck
self.stats = None
self.width = width
self.height = height
self.dpi = dpi
- self.selective = selective
def calcStats (self):
if not self.stats:
@@ -71,7 +70,7 @@
months = {}
next = {}
lowestInDay = 0
- self.endOfDay = self.deck.failedCutoff
+ self.endOfDay = time.time() + 43200 # BUG: in half a day
t = time.time()
young = """
select interval, combinedDue from cards c
@@ -79,11 +78,6 @@
mature = """
select interval, combinedDue
from cards c where relativeDelay = 1 and type >= 0 and interval > 21"""
- if self.selective:
- young = self.deck._cardLimit("revActive", "revInactive",
- young)
- mature = self.deck._cardLimit("revActive", "revInactive",
- mature)
young = self.deck.s.all(young)
mature = self.deck.s.all(mature)
for (src, dest) in [(young, daysYoung),
@@ -117,7 +111,7 @@
from stats
where type = 1""")
- todaydt = self.deck._dailyStats.day
+ todaydt = anki.stats.raiseStats(self.deck.s, today=True).day
for dest, source in [("dayRepsNew", "combinedNewReps"),
("dayRepsYoung", "combinedYoungReps"),
("dayRepsMature", "matureReps")]:
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/history.py anki-1.2.8/libanki/anki/history.py
--- anki-1.2.8.orig/libanki/anki/history.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/history.py 2012-04-01 02:00:49.043284334 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -35,9 +36,7 @@
class CardHistoryEntry(object):
"Create after rescheduling card."
- def __init__(self, card=None, ease=None, delay=None):
- if not card:
- return
+ def __init__(self, card, ease, delay):
self.cardId = card.id
self.lastInterval = card.lastInterval
self.nextInterval = card.interval
@@ -49,27 +48,6 @@
self.ease = ease
self.delay = delay
self.thinkingTime = card.thinkingTime()
-
- def writeSQL(self, s):
- s.statement("""
-insert into reviewHistory
-(cardId, lastInterval, nextInterval, ease, delay, lastFactor,
-nextFactor, reps, thinkingTime, yesCount, noCount, time)
-values (
-:cardId, :lastInterval, :nextInterval, :ease, :delay,
-:lastFactor, :nextFactor, :reps, :thinkingTime, :yesCount, :noCount,
-:time)""",
- cardId=self.cardId,
- lastInterval=self.lastInterval,
- nextInterval=self.nextInterval,
- ease=self.ease,
- delay=self.delay,
- lastFactor=self.lastFactor,
- nextFactor=self.nextFactor,
- reps=self.reps,
- thinkingTime=self.thinkingTime,
- yesCount=self.yesCount,
- noCount=self.noCount,
- time=time.time())
+ self.time = time.time()
mapper(CardHistoryEntry, reviewHistoryTable)
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/hooks.py anki-1.2.8/libanki/anki/hooks.py
--- anki-1.2.8.orig/libanki/anki/hooks.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/hooks.py 2012-03-29 14:19:54.993016275 +0800
@@ -18,18 +18,18 @@
_hooks = {}
-def runHook(hook, *args):
+def runHook(hook, *args, **kwargs):
"Run all functions on hook."
hook = _hooks.get(hook, None)
if hook:
for func in hook:
- func(*args)
+ func(*args, **kwargs)
-def runFilter(hook, arg, *args):
+def runFilter(hook, arg, *args, **kwargs):
hook = _hooks.get(hook, None)
if hook:
for func in hook:
- arg = func(arg, *args)
+ arg = func(arg, *args, **kwargs)
return arg
def addHook(hook, func):
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/importing/__init__.py anki-1.2.8/libanki/anki/importing/__init__.py
--- anki-1.2.8.orig/libanki/anki/importing/__init__.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/importing/__init__.py 2012-03-27 22:27:49.173310170 +0800
@@ -61,14 +61,9 @@
self.deck.startProgress(num)
self.deck.updateProgress(_("Importing..."))
c = self.foreignCards()
- if self.importCards(c):
+ if self.importCards(c) and random:
self.deck.updateProgress()
- self.deck.updateCardTags(self.cardIds)
- self.deck.updateProgress()
- self.deck.updatePriorities(self.cardIds)
- if random:
- self.deck.updateProgress()
- self.deck.randomizeNewCards(self.cardIds)
+ self.deck.randomizeNewCards(self.cardIds)
self.deck.finishProgress()
if c:
self.deck.setModified()
@@ -140,14 +135,6 @@
data)
# rebuild caches
self.deck.updateProgress()
- cids = self.deck.s.column0(
- "select id from cards where factId in %s" %
- ids2str(fids))
- self.deck.updateCardTags(cids)
- self.deck.updateProgress()
- self.deck.updatePriorities(cids)
- self.deck.updateProgress()
- self.deck.updateCardsFromFactIds(fids)
self.total = len(fids)
self.deck.setModified()
self.deck.finishProgress()
@@ -242,7 +229,6 @@
[fudgeCreated({'modelId': self.model.id,
'tags': canonifyTags(self.tagsToAdd + " " + cards[n].tags),
'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]))
@@ -280,8 +266,6 @@
self.deck.s.execute(cardsTable.insert(),
data)
self.deck.updateProgress()
- self.deck.updateCardsFromFactIds(factIds)
- self.deck.cardCount += len(cards) * active
self.total = len(factIds)
def addMeta(self, data, card):
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/__init__.py anki-1.2.8/libanki/anki/__init__.py
--- anki-1.2.8.orig/libanki/anki/__init__.py 2011-03-28 15:38:38.000000000 +0800
+++ anki-1.2.8/libanki/anki/__init__.py 2012-03-29 14:12:40.246860482 +0800
@@ -54,5 +54,6 @@
pass
version = "1.2.8"
+loadPanki = False
from anki.deck import DeckStorage
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/latex.py anki-1.2.8/libanki/anki/latex.py
--- anki-1.2.8.orig/libanki/anki/latex.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/latex.py 2012-03-29 14:20:15.473117828 +0800
@@ -133,4 +133,4 @@
return renderLatex(deck, html)
# setup q/a filter
-addHook("formatQA", formatQA)
+#addHook("formatQA", formatQA)
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/models.py anki-1.2.8/libanki/anki/models.py
--- anki-1.2.8.orig/libanki/anki/models.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/models.py 2012-03-29 15:13:22.188919898 +0800
@@ -132,33 +132,6 @@
mapper(CardModel, cardModelsTable)
-def formatQA(cid, mid, fact, tags, cm, deck):
- "Return a dict of {id, question, answer}"
- d = {'id': cid}
- fields = {}
- for (k, v) in fact.items():
- fields["text:"+k] = stripHTML(v[1])
- if v[1]:
- fields[k] = '%s' % (
- hexifyID(v[0]), v[1])
- else:
- fields[k] = u""
- fields['tags'] = tags[0]
- fields['Tags'] = tags[0]
- fields['modelTags'] = tags[1]
- fields['cardModel'] = tags[2]
- # render q & a
- ret = []
- for (type, format) in (("question", cm.qformat),
- ("answer", cm.aformat)):
- # convert old style
- format = re.sub("%\((.+?)\)s", "{{\\1}}", format)
- # allow custom rendering functions & info
- fields = runFilter("prepareFields", fields, cid, mid, fact, tags, cm, deck)
- html = render(format, fields)
- d[type] = runFilter("formatQA", html, type, cid, mid, fact, tags, cm, deck)
- return d
-
# Model table
##########################################################################
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/schedulers.py anki-1.2.8/libanki/anki/schedulers.py
--- anki-1.2.8.orig/libanki/anki/schedulers.py 1970-01-01 08:00:00.000000000 +0800
+++ anki-1.2.8/libanki/anki/schedulers.py 2012-04-01 01:53:04.116978886 +0800
@@ -0,0 +1,144 @@
+# coding: utf-8
+# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import time, random
+import anki
+from anki.cards import Card
+
+class StandardScheduler(object):
+
+ def __init__(self, deck, spacedFactsMax=20):
+ self._deck = deck
+ self._db = deck.s
+ self._spacedFacts = []
+ self._spacedFactsMax = spacedFactsMax
+ self._reviewCardsServed = 0
+ self._totalCardsServed = 0
+ # determine starting factor for new cards
+ initialFactor = 2.5
+ minimumAverage = 1.7
+ averageFactor = (deck.s.scalar(
+ "select avg(factor) from cards where type = 1") or initialFactor)
+ self._averageFactor = max(averageFactor, minimumAverage)
+
+ def getCard(self):
+ "Return the next card object, or None."
+ card = self._nextCard()
+ if not card:
+ return card
+ self._totalCardsServed += 1
+ if card.type == 1:
+ self._reviewCardsServed += 1
+ self._spacedFacts.append(card.factId)
+ if len(self._spacedFacts) > self._spacedFactsMax:
+ del self._spacedFacts[0]
+ return card
+
+ def _nextCard(self):
+ best = (None, len(self._spacedFacts))
+ limit = self._db.query(anki.models.CardModel).count() * len(self._spacedFacts) + 1
+ def queues():
+ query = self._db.query(Card).order_by(Card.combinedDue)
+ newQuery = query.filter(Card.type == 2)
+ revQuery = query.filter(Card.type.in_((0, 1))).filter(Card.combinedDue < time.time())
+ doReview = self._totalCardsServed and \
+ self._reviewCardsServed / float(self._totalCardsServed) < 0.8
+ # queue, still fish for the best
+ yield (newQuery, revQuery)[doReview], True
+ yield (revQuery, newQuery)[doReview], best[0] is None
+ for query, checkForBest in queues():
+ cards = query.limit(limit).all()
+ for c in cards:
+ if c.factId not in self._spacedFacts:
+ return c
+ elif checkForBest:
+ index = self._spacedFacts.index(c.factId)
+ if index < best[1]:
+ best = (c, index)
+ return best[0]
+
+ def updateAnsweredCard(self, card, ease):
+ card.interval, card.lastInterval = self.nextInterval(card, ease), card.interval
+ self._updateFactors(card, ease)
+ if ease > 1:
+ card.type = 1
+ card.relativeDelay = card.type
+ card.lastDue = card.combinedDue
+ card.combinedDue = card.due = card.interval * 86400.0 + time.time()
+
+
+ def nextInterval(self, card, ease):
+ "Return the next interval for CARD given EASE."
+ if card.reps and card.successive:
+ delay = (time.time() - card.combinedDue) / 86400.0
+ else:
+ delay = 0
+ interval = card.interval
+ factor = card.factor
+ # if shown early
+ if delay < 0:
+ # FIXME: this should recreate lastInterval from interval /
+ # lastFactor, or we lose delay information when reviewing early
+ interval = max(card.lastInterval, card.interval + delay)
+ if interval < self._deck.midIntervalMin:
+ interval = 0
+ delay = 0
+ # if interval is less than mid interval, use presets
+ if ease == 1:
+ interval *= self._deck.delay2
+ if interval < self._deck.hardIntervalMin:
+ interval = 0
+ elif interval == 0:
+ if ease == 2:
+ interval = random.uniform(self._deck.hardIntervalMin,
+ self._deck.hardIntervalMax)
+ elif ease == 3:
+ interval = random.uniform(self._deck.midIntervalMin,
+ self._deck.midIntervalMax)
+ elif ease == 4:
+ interval = random.uniform(self._deck.easyIntervalMin,
+ self._deck.easyIntervalMax)
+ else:
+ # if not cramming, boost initial 2
+ if (interval < self._deck.hardIntervalMax and
+ interval > 0.166):
+ mid = (self._deck.midIntervalMin + self._deck.midIntervalMax) / 2.0
+ interval = mid / factor
+ # multiply last interval by factor
+ if ease == 2:
+ interval = (interval + delay/4) * 1.2
+ elif ease == 3:
+ interval = (interval + delay/2) * factor
+ elif ease == 4:
+ interval = (interval + delay) * factor * 1.3
+ interval *= random.uniform(0.95, 1.05)
+ return interval
+
+ def _updateFactors(self, card, ease):
+ "Update CARD's factor based on EASE."
+ card.lastFactor = card.factor
+ if not card.reps:
+ # card is new, inherit beginning factor
+ card.factor = self._averageFactor
+ if card.successive and not card.lastInterval < 7:
+ if ease == 1:
+ card.factor -= 0.20
+ elif ease == 2:
+ card.factor -= 0.15
+ if ease == 4:
+ card.factor += 0.10
+ card.factor = max(1.3, card.factor)
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/sound.py anki-1.2.8/libanki/anki/sound.py
--- anki-1.2.8.orig/libanki/anki/sound.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/sound.py 2012-04-01 02:01:03.419355619 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -131,46 +132,35 @@
mplayerEvt = threading.Event()
mplayerClear = False
-class MplayerReader(threading.Thread):
- "Read any debugging info to prevent mplayer from blocking."
-
- def run(self):
- while 1:
- mplayerEvt.wait()
- try:
- mplayerManager.mplayer.stdout.read()
- except:
- pass
class MplayerMonitor(threading.Thread):
+ def commandMplayer(self, cmd):
+ cmd += '\n'
+ if not self.mplayer:
+ self.startProcess()
+ try:
+ self.mplayer.stdin.write(cmd)
+ except:
+ # mplayer has quit and needs restarting
+ self.deadPlayers.append(self.mplayer)
+ self.mplayer = None
+ self.startProcess()
+ self.mplayer.stdin.write(cmd)
+
def run(self):
- global mplayerClear
+ global mplayerQueue, mplayerClear
self.mplayer = None
self.deadPlayers = []
while 1:
mplayerEvt.wait()
- if mplayerQueue:
- # ensure started
- if not self.mplayer:
- self.startProcess()
- # loop through files to play
- while mplayerQueue:
- item = mplayerQueue.pop(0)
- if mplayerClear:
- mplayerClear = False
- extra = ""
- else:
- extra = " 1"
- cmd = 'loadfile "%s"%s\n' % (item, extra)
- try:
- self.mplayer.stdin.write(cmd)
- except:
- # mplayer has quit and needs restarting
- self.deadPlayers.append(self.mplayer)
- self.mplayer = None
- self.startProcess()
- self.mplayer.stdin.write(cmd)
+ if mplayerClear:
+ self.commandMplayer('stop')
+ mplayerQueue = []
+ mplayerClear = False
+ while mplayerQueue:
+ item = mplayerQueue.pop(0)
+ self.commandMplayer('loadfile "%s" 1' % (item))
# wait() on finished processes. we don't want to block on the
# wait, so we keep trying each time we're reactivated
def clean(pl):
@@ -197,7 +187,7 @@
cmd = mplayerCmd + ["-slave", "-idle"]
self.mplayer = subprocess.Popen(
cmd, startupinfo=si, stdin=subprocess.PIPE,
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ stderr=file(os.devnull, 'w'))
except OSError:
mplayerEvt.clear()
raise Exception("Audio player not found")
@@ -230,14 +220,11 @@
mplayerEvt.set()
def ensureMplayerThreads():
- global mplayerManager, mplayerReader
+ global mplayerManager
if not mplayerManager:
mplayerManager = MplayerMonitor()
mplayerManager.daemon = True
mplayerManager.start()
- mplayerReader = MplayerReader()
- mplayerReader.daemon = True
- mplayerReader.start()
def stopMplayer():
if not mplayerManager:
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/stats.py anki-1.2.8/libanki/anki/stats.py
--- anki-1.2.8.orig/libanki/anki/stats.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/stats.py 2012-04-01 02:01:16.991422919 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -9,16 +10,15 @@
__docformat__ = 'restructuredtext'
# we track statistics over the life of the deck, and per-day
-STATS_LIFE = 0
-STATS_DAY = 1
import unicodedata, time, sys, os, datetime
import anki, anki.utils
from datetime import date
from anki.db import *
from anki.lang import _, ngettext
-from anki.utils import canonifyTags, ids2str
+from anki.utils import canonifyTags, ids2str, fmtTimeSpan
from anki.hooks import runFilter
+from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
# Tracking stats on the DB
##########################################################################
@@ -51,147 +51,45 @@
Column('matureEase4', Integer, nullable=False, default=0))
class Stats(object):
- def __init__(self):
- self.day = None
- self.reps = 0
- self.averageTime = 0
- self.reviewTime = 0
- self.distractedTime = 0
- self.distractedReps = 0
- self.newEase0 = 0
- self.newEase1 = 0
- self.newEase2 = 0
- self.newEase3 = 0
- self.newEase4 = 0
- self.youngEase0 = 0
- self.youngEase1 = 0
- self.youngEase2 = 0
- self.youngEase3 = 0
- self.youngEase4 = 0
- self.matureEase0 = 0
- self.matureEase1 = 0
- self.matureEase2 = 0
- self.matureEase3 = 0
- self.matureEase4 = 0
-
- def fromDB(self, s, id):
- r = s.first("select * from stats where id = :id", id=id)
- (self.id,
- self.type,
- self.day,
- self.reps,
- self.averageTime,
- self.reviewTime,
- self.distractedTime,
- self.distractedReps,
- self.newEase0,
- self.newEase1,
- self.newEase2,
- self.newEase3,
- self.newEase4,
- self.youngEase0,
- self.youngEase1,
- self.youngEase2,
- self.youngEase3,
- self.youngEase4,
- self.matureEase0,
- self.matureEase1,
- self.matureEase2,
- self.matureEase3,
- self.matureEase4) = r
- self.day = datetime.date(*[int(i) for i in self.day.split("-")])
-
- def create(self, s, type, day):
+ def __init__(self, type, day):
self.type = type
self.day = day
- s.execute("""insert into stats
-(type, day, reps, averageTime, reviewTime, distractedTime, distractedReps,
-newEase0, newEase1, newEase2, newEase3, newEase4, youngEase0, youngEase1,
-youngEase2, youngEase3, youngEase4, matureEase0, matureEase1, matureEase2,
-matureEase3, matureEase4) values (:type, :day, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)""", self.__dict__)
- self.id = s.scalar(
- "select id from stats where type = :type and day = :day",
- type=type, day=day)
-
- def toDB(self, s):
- assert self.id
- s.execute("""update stats set
-type=:type,
-day=:day,
-reps=:reps,
-averageTime=:averageTime,
-reviewTime=:reviewTime,
-newEase0=:newEase0,
-newEase1=:newEase1,
-newEase2=:newEase2,
-newEase3=:newEase3,
-newEase4=:newEase4,
-youngEase0=:youngEase0,
-youngEase1=:youngEase1,
-youngEase2=:youngEase2,
-youngEase3=:youngEase3,
-youngEase4=:youngEase4,
-matureEase0=:matureEase0,
-matureEase1=:matureEase1,
-matureEase2=:matureEase2,
-matureEase3=:matureEase3,
-matureEase4=:matureEase4
-where id = :id""", self.__dict__)
mapper(Stats, statsTable)
-def genToday(deck):
- return datetime.datetime.utcfromtimestamp(
- time.time() - deck.utcOffset).date()
-
-def updateAllStats(s, gs, ds, card, ease, oldState):
+def updateAllStats(s, card, ease):
"Update global and daily statistics."
- updateStats(s, gs, card, ease, oldState)
- updateStats(s, ds, card, ease, oldState)
+ for today in False, True:
+ updateStats(s, today, card, ease)
-def updateStats(s, stats, card, ease, oldState):
+def updateStats(dbsession, today, card, ease):
+ stats = raiseStats(dbsession, today=today)
stats.reps += 1
- delay = card.totalTime()
+ delay = card.thinkingTime()
if delay >= 60:
stats.reviewTime += 60
else:
stats.reviewTime += delay
- stats.averageTime = (
- stats.reviewTime / float(stats.reps))
+ stats.averageTime = (stats.reviewTime / float(stats.reps))
# update eases
- attr = oldState + "Ease%d" % ease
+ attr = card.state() + "Ease%d" % ease
setattr(stats, attr, getattr(stats, attr) + 1)
- stats.toDB(s)
-def globalStats(deck):
- s = deck.s
- type = STATS_LIFE
- today = genToday(deck)
- id = s.scalar("select id from stats where type = :type",
- type=type)
- stats = Stats()
- if id:
- stats.fromDB(s, id)
- return stats
- else:
- stats.create(s, type, today)
- stats.type = type
- return stats
-
-def dailyStats(deck):
- s = deck.s
- type = STATS_DAY
- today = genToday(deck)
- id = s.scalar("select id from stats where type = :type and day = :day",
- type=type, day=today)
- stats = Stats()
- if id:
- stats.fromDB(s, id)
+def raiseStats(dbsession, today=False):
+ if today is True:
+ utcOffset = dbsession.query(anki.deck.Deck).get(1).utcOffset
+ today = datetime.datetime.utcfromtimestamp(time.time() - utcOffset).date()
+ type = 1 if today else 0
+ query = dbsession.query(Stats).filter_by(type=type)
+ if today:
+ query = query.filter_by(day=today)
+ try:
+ return query.one()
+ except NoResultFound:
+ stats = Stats(type, today)
+ dbsession.add(stats)
+ dbsession.flush()
return stats
- else:
- stats.create(s, type, today)
- return stats
def summarizeStats(stats, pre=""):
"Generate percentages and total counts for STATS. Optionally prefix."
@@ -241,11 +139,11 @@
except ZeroDivisionError:
h[a + "%"] = 0
-def getStats(s, gs, ds):
+def getStatistics(dbsession):
"Return a handy dictionary exposing a number of internal stats."
h = {}
- h.update(summarizeStats(gs, "g"))
- h.update(summarizeStats(ds, "d"))
+ for prefix, today in ('g', False), ('d', True):
+ h.update(summarizeStats(raiseStats(dbsession, today), prefix))
return h
# Card stats
@@ -259,7 +157,7 @@
def report(self):
c = self.card
- fmt = anki.utils.fmtTimeSpan
+ fmt = fmtTimeSpan
fmtFloat = anki.utils.fmtFloat
self.txt = ""
self.addLine(_("Added"), self.strTime(c.created))
@@ -298,7 +196,7 @@
self.txt += "%s | %s |
" % (k, v)
def strTime(self, tm):
- s = anki.utils.fmtTimeSpan(time.time() - tm)
+ s = fmtTimeSpan(time.time() - tm)
return _("%s ago") % s
# Deck stats (specific to the 'sched' scheduler)
@@ -317,9 +215,9 @@
return _("Please add some cards first.") + ""
d = self.deck
html="" + _("Deck Statistics") + "
"
- html += _("Deck created: %s ago
") % self.createdTimeStr()
- total = d.cardCount
- new = d.newCountAll()
+ html += _("Deck created: %s ago
") % fmtTimeSpan(time.time() - d.created)
+ total = d.cardCount()
+ new = d.newCount()
young = d.youngCardCount()
old = d.matureCardCount()
newP = new / float(total) * 100
@@ -359,7 +257,7 @@
'totalSum' : stats['gNewTotal'] } + "
")
# average pending time
- existing = d.cardCount - d.newCountToday
+ existing = total - new
def tr(a, b):
return "%s | %s |
" % (a, b)
def repsPerDay(reps,days):
@@ -494,39 +392,10 @@
"select sum(interval) / count(interval) from cards "
"where cards.reps > 0") or 0
- def intervalReport(self, intervals, labels, total):
- boxes = self.splitIntoIntervals(intervals)
- keys = boxes.keys()
- keys.sort()
- html = ""
- for key in keys:
- html += ("%s | " +
- "%d | %s |
") % (
- labels[key],
- boxes[key],
- fmtPerc(boxes[key] / float(total) * 100))
- return html
-
- def splitIntoIntervals(self, intervals):
- boxes = {}
- n = 0
- for i in range(len(intervals) - 1):
- (min, max) = (intervals[i], intervals[i+1])
- for c in self.deck:
- if c.interval > min and c.interval <= max:
- boxes[n] = boxes.get(n, 0) + 1
- n += 1
- return boxes
-
def newAverage(self):
"Average number of new cards added each day."
- return self.deck.cardCount / max(1, self.ageInDays())
-
- def createdTimeStr(self):
- return anki.utils.fmtTimeSpan(time.time() - self.deck.created)
-
- def ageInDays(self):
- return (time.time() - self.deck.created) / 86400.0
+ ageInDays = (time.time() - self.deck.created) / 86400.0
+ return self.deck.cardCount() / max(1, ageInDays)
def getSumInverseRoundInterval(self):
return self.deck.s.scalar(
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/sync.py anki-1.2.8/libanki/anki/sync.py
--- anki-1.2.8.orig/libanki/anki/sync.py 2011-03-28 15:29:18.000000000 +0800
+++ anki-1.2.8/libanki/anki/sync.py 2012-04-01 02:01:27.159473346 +0800
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
+# Copyright 2012 Robert Siemer
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
"""\
@@ -25,15 +26,13 @@
import zlib, re, urllib, urllib2, socket, simplejson, time, shutil
import os, base64, httplib, sys, tempfile, httplib, types
from datetime import date
-import anki, anki.deck, anki.cards
+import anki, anki.deck, anki.cards, anki.stats
from anki.db import sqlite
from anki.errors import *
from anki.models import Model, FieldModel, CardModel
from anki.facts import Fact, Field
from anki.cards import Card
-from anki.stats import Stats, globalStats
-from anki.history import CardHistoryEntry
-from anki.stats import globalStats
+from anki.stats import raiseStats, Stats
from anki.utils import ids2str, hexifyID, checksum
from anki.media import mediaFiles
from anki.lang import _
@@ -101,7 +100,6 @@
def __init__(self, deck=None):
self.deck = deck
self.diffs = {}
- self.serverExcludedTags = []
self.timediff = 0
# Control
@@ -118,7 +116,6 @@
payload = self.genPayload(sums)
res = self.server.applyPayload(payload)
self.applyPayloadReply(res)
- self.deck.reset()
def prepareSync(self, timediff):
"Sync setup. True if sync needed."
@@ -138,7 +135,6 @@
def genPayload(self, summaries):
(lsum, rsum) = summaries
- self.preSyncRefresh()
payload = {}
# first, handle models, facts and cards
for key in KEYS:
@@ -158,7 +154,6 @@
def applyPayload(self, payload):
reply = {}
- self.preSyncRefresh()
# model, facts and cards
for key in KEYS:
k = 'added-' + key
@@ -182,10 +177,6 @@
if 'sources' in payload:
self.updateSources(payload['sources'])
self.postSyncRefresh()
- cardIds = [x[0] for x in payload['added-cards']]
- self.deck.updateCardTags(cardIds)
- # rebuild priorities on server
- self.rebuildPriorities(cardIds, self.serverExcludedTags)
return reply
def applyPayloadReply(self, reply):
@@ -194,7 +185,7 @@
k = 'added-' + key
# old version may not send media
if k in reply:
- self.updateObjsFromKey(reply['added-' + key], key)
+ self.updateObjsFromKey(reply[k], key)
# deck
if 'deck' in reply:
self.updateDeck(reply['deck'])
@@ -203,10 +194,6 @@
if 'sources' in reply:
self.updateSources(reply['sources'])
self.postSyncRefresh()
- # rebuild priorities on client
- cardIds = [x[0] for x in reply['added-cards']]
- self.deck.updateCardTags(cardIds)
- self.rebuildPriorities(cardIds)
if self.missingFacts() != 0:
raise Exception(
"Facts missing after sync. Please run Tools>Advanced>Check DB.")
@@ -216,20 +203,12 @@
"select count() from cards where factId "+
"not in (select id from facts)");
- def rebuildPriorities(self, cardIds, suspend=[]):
- self.deck.updateAllPriorities(partial=True, dirty=False)
- self.deck.updatePriorities(cardIds, suspend=suspend, dirty=False)
-
def postSyncRefresh(self):
"Flush changes to DB, and reload object associations."
self.deck.s.flush()
self.deck.s.refresh(self.deck)
self.deck.currentModel
- def preSyncRefresh(self):
- # ensure global stats are available (queue may not be built)
- self.deck._globalStats = globalStats(self.deck)
-
def payloadChanges(self, payload):
h = {
'lf': len(payload['added-facts']['facts']),
@@ -622,22 +601,12 @@
# and ensure lastSync is greater than modified
self.deck.lastSync = max(time.time(), self.deck.modified+1)
d = self.dictFromObj(self.deck)
- del d['Session']
- del d['engine']
- del d['s']
- del d['path']
- del d['syncName']
- del d['version']
- if 'newQueue' in d:
- del d['newQueue']
- del d['failedQueue']
- del d['revQueue']
- # these may be deleted before bundling
- if 'css' in d: del d['css']
- if 'models' in d: del d['models']
- if 'currentModel' in d: del d['currentModel']
- keys = d.keys()
- for k in keys:
+ for k in ('Session', 'engine', 's', 'path', 'syncName', 'version',
+ 'newQueue', 'failedQueue', 'revQueue', 'css', 'models',
+ 'currentModel', 'spacedFacts'):
+ if k in d:
+ del d[k]
+ for k in d.keys():
if isinstance(d[k], types.MethodType):
del d[k]
d['meta'] = self.realLists(self.deck.s.all("select * from deckVars"))
@@ -662,32 +631,20 @@
lastDay = date.fromtimestamp(max(0, self.deck.lastSync - 60*60*24))
ids = self.deck.s.column0(
"select id from stats where type = 1 and day >= :day", day=lastDay)
- stat = Stats()
- def statFromId(id):
- stat.fromDB(self.deck.s, id)
- return stat
stats = {
- 'global': bundleStat(self.deck._globalStats),
- 'daily': [bundleStat(statFromId(id)) for id in ids],
+ 'global': bundleStat(raiseStats(self.deck.s)),
+ 'daily': [bundleStat(self.deck.s.query(Stats).get(id)) for id in ids],
}
return stats
def updateStats(self, stats):
stats['global']['day'] = date.fromordinal(stats['global']['day'])
- self.applyDict(self.deck._globalStats, stats['global'])
- self.deck._globalStats.toDB(self.deck.s)
+ self.applyDict(raiseStats(self.deck.s), stats['global'])
for record in stats['daily']:
record['day'] = date.fromordinal(record['day'])
- stat = Stats()
- id = self.deck.s.scalar("select id from stats where "
- "type = :type and day = :day",
- type=1, day=record['day'])
- if id:
- stat.fromDB(self.deck.s, id)
- else:
- stat.create(self.deck.s, 1, record['day'])
+ stat = raiseStats(self.deck.s, today=record['day'])
self.applyDict(stat, record)
- stat.toDB(self.deck.s)
+ self.deck.s.flush()
def bundleHistory(self):
return self.realLists(self.deck.s.all("""
@@ -782,7 +739,6 @@
"Sync two decks one way."
payload = self.server.genOneWayPayload(lastSync)
self.applyOneWayPayload(payload)
- self.deck.reset()
def syncOneWayDeckName(self):
return (self.deck.s.scalar("select name from sources where id = :id",
@@ -874,20 +830,7 @@
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, "", "", 2.5, 0, 0, 2, :t, 2)""", dlist)
- # update q/as
- 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])
- # rebuild priorities on client
- cardIds = [c[0] for c in cards]
- self.deck.updateCardTags(cardIds)
- self.rebuildPriorities(cardIds)
# Tools
##########################################################################
diff -Nurx '*.py~' anki-1.2.8.orig/libanki/anki/utils.py anki-1.2.8/libanki/anki/utils.py
--- anki-1.2.8.orig/libanki/anki/utils.py 2011-03-28 15:14:17.000000000 +0800
+++ anki-1.2.8/libanki/anki/utils.py 2012-04-01 14:41:56.847338549 +0800
@@ -126,6 +126,11 @@
# HTML
##############################################################################
+_cleanNameRe = re.compile(r'[\\ "&]')
+def cleanName(name):
+ 'remove space, backslash, double quote and ampersand'
+ return _cleanNameRe.sub('', name)
+
def stripHTML(s):
s = re.sub("(?s).*?", "", s)
s = re.sub("(?s).*?", "", s)