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