diff --git a/anki/cards.py b/anki/cards.py index 7aa25978c..ee9f7b8c1 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import pprint import time from anki.utils import intTime, timestampID, joinFields @@ -106,6 +107,7 @@ insert or replace into cards values self.odid, self.flags, self.data) + self.col.log(self) def flushSched(self): self.mod = intTime() @@ -121,6 +123,7 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""", self.mod, self.usn, self.type, self.queue, self.due, self.ivl, self.factor, self.reps, self.lapses, self.left, self.odue, self.odid, self.did, self.id) + self.col.log(self) def q(self, reload=False, browser=False): return self.css() + self._getQA(reload, browser)['q'] @@ -180,3 +183,12 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""", self.model(), joinFields(self.note().fields)) if self.ord not in ords: return True + + def __repr__(self): + d = dict(self.__dict__) + # remove non-useful elements + del d['_note'] + del d['_qa'] + del d['col'] + del d['timerStarted'] + return pprint.pformat(d, width=300) diff --git a/anki/collection.py b/anki/collection.py index dcb74c7b7..2af345bb9 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -51,6 +51,7 @@ class _Collection(object): def __init__(self, db, server=False): self.db = db self.path = db._path + self.log(self.path, anki.version) self.server = server self._lastSave = time.time() self.clearUndo() @@ -204,8 +205,11 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", # Object creation helpers ########################################################################## - def getCard(self, id): - return anki.cards.Card(self, id) + def getCard(self, id, log=True): + c = anki.cards.Card(self, id) + if log: + self.log(c, stack=1) + return c def getNote(self, id): return anki.notes.Note(self, id=id) @@ -767,3 +771,9 @@ and queue = 0""", intTime(), self.usn()) self.db.execute("vacuum") self.db.execute("analyze") self.lock() + + # Logging + ########################################################################## + + def log(self, *args, **kwargs): + runHook("log", args, kwargs) diff --git a/anki/decks.py b/anki/decks.py index 41fe720ed..aa170b670 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -71,6 +71,8 @@ defaultConf = { 'minSpace': 1, # not currently used 'ivlFct': 1, 'maxIvl': 36500, + # may not be set on old decks + 'bury': True, }, 'maxTaken': 60, 'timer': 0, diff --git a/anki/media.py b/anki/media.py index 1b1bea5aa..5818770d4 100644 --- a/anki/media.py +++ b/anki/media.py @@ -225,7 +225,7 @@ class MediaManager(object): nfcFile = unicodedata.normalize("NFC", file) # we enforce NFC fs encoding on non-macs; on macs we'll have gotten # NFD so we use the above variable for comparing references - if not isMac: + if not isMac and local: if file != nfcFile: # delete if we already have the NFC form, otherwise rename if os.path.exists(nfcFile): diff --git a/anki/notes.py b/anki/notes.py index 16be73e0a..d1f54a7b6 100644 --- a/anki/notes.py +++ b/anki/notes.py @@ -69,7 +69,7 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""", return joinFields(self.fields) def cards(self): - return [self.col.getCard(id) for id in self.col.db.list( + return [self.col.getCard(id, log=False) for id in self.col.db.list( "select id from cards where nid = ? order by ord", self.id)] def model(self): diff --git a/anki/sched.py b/anki/sched.py index 8fc61dff6..99fffa382 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -3,9 +3,12 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import division -import time, random, itertools +import time +import random +import itertools from operator import itemgetter from heapq import * + #from anki.cards import Card from anki.utils import ids2str, intTime, fmtTimeSpan from anki.lang import _ @@ -52,6 +55,7 @@ class Scheduler(object): self._haveQueues = True def answerCard(self, card, ease): + self.col.log() assert ease >= 1 and ease <= 4 self.col.markReview(card) if self._burySiblingsOnAnswer: @@ -139,14 +143,19 @@ order by due""" % self._deckLimit(), def unburyCards(self): "Unbury cards." self.col.conf['lastUnburied'] = self.today + self.col.log( + self.col.db.list("select id from cards where queue = -2")) self.col.db.execute( - "update cards set mod=?,usn=?,queue=type where queue = -2", - intTime(), self.col.usn()) + "update cards set queue=type where queue = -2") def unburyCardsForDeck(self): + sids = ids2str(self.col.decks.active()) + self.col.log( + self.col.db.list("select id from cards where queue = -2 and did in %s" + % sids)) self.col.db.execute( "update cards set mod=?,usn=?,queue=type where queue = -2 and did in %s" - % ids2str(self.col.decks.active()), intTime(), self.col.usn()) + % sids, intTime(), self.col.usn()) # Rev/lrn/time daily stats ########################################################################## @@ -943,12 +952,14 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" ids = [] return ids # move the cards over + self.col.log(deck['id'], ids) self._moveToDyn(deck['id'], ids) return ids def emptyDyn(self, did, lim=None): if not lim: lim = "did = %s" % did + self.col.log(self.col.db.list("select id from cards where %s" % lim)) # move out of cram queue self.col.db.execute(""" update cards set did = odid, queue = (case when type = 1 then 0 @@ -1111,6 +1122,7 @@ did = ?, queue = %s, due = ?, mod = ?, usn = ? where id = ?""" % queue, data) self.today = int((time.time() - self.col.crt) // 86400) # end of day cutoff self.dayCutoff = self.col.crt + (self.today+1)*86400 + self.col.log(self.today, self.dayCutoff) # update all daily counts, but don't save decks to prevent needless # conflicts. we'll save on card answer instead def update(g): @@ -1239,6 +1251,7 @@ To study outside of the normal schedule, click the Custom Study button below.""" def suspendCards(self, ids): "Suspend cards." + self.col.log(ids) self.remFromDyn(ids) self.removeLrn(ids) self.col.db.execute( @@ -1247,6 +1260,7 @@ To study outside of the normal schedule, click the Custom Study button below.""" def unsuspendCards(self, ids): "Unsuspend cards." + self.col.log(ids) self.col.db.execute( "update cards set queue=type,mod=?,usn=? " "where queue = -1 and id in "+ ids2str(ids), @@ -1256,6 +1270,7 @@ To study outside of the normal schedule, click the Custom Study button below.""" "Bury all cards for note until next session." cids = self.col.db.list( "select id from cards where nid = ? and queue >= 0", nid) + self.col.log(cids) self.removeLrn(cids) self.col.db.execute(""" update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids), @@ -1266,15 +1281,19 @@ update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids), def _burySiblings(self, card): toBury = [] - conf = self._newConf(card) - buryNew = conf.get("bury", True) + nconf = self._newConf(card) + buryNew = nconf.get("bury", True) + rconf = self._revConf(card) + buryRev = rconf.get("bury", True) # loop through and remove from queues for cid,queue in self.col.db.execute(""" select id, queue from cards where nid=? and id!=? and (queue=0 or (queue=2 and due<=?))""", card.nid, card.id, self.today): if queue == 2: - toBury.append(cid) + if buryRev: + toBury.append(cid) + # if bury disabled, we still discard to give same-day spacing try: self._revQueue.remove(cid) except ValueError: @@ -1288,9 +1307,11 @@ and (queue=0 or (queue=2 and due<=?))""", except ValueError: pass # then bury - self.col.db.execute( - "update cards set queue=-2,mod=?,usn=? where id in "+ids2str(toBury), - intTime(), self.col.usn()) + if toBury: + self.col.db.execute( + "update cards set queue=-2,mod=?,usn=? where id in "+ids2str(toBury), + intTime(), self.col.usn()) + self.col.log(toBury) # Resetting ########################################################################## @@ -1304,6 +1325,7 @@ and (queue=0 or (queue=2 and due<=?))""", "select max(due) from cards where type=0") or 0 # takes care of mod + usn self.sortCards(ids, start=pmax+1) + self.col.log(ids) def reschedCards(self, ids, imin, imax): "Put cards in review queue with a new interval in days (min, max)." @@ -1319,6 +1341,7 @@ and (queue=0 or (queue=2 and due<=?))""", update cards set type=2,queue=2,ivl=:ivl,due=:due, usn=:usn, mod=:mod, factor=:fact where id=:id and odid=0 and queue >=0""", d) + self.col.log(ids) def resetCards(self, ids): "Completely reset cards for export." @@ -1328,6 +1351,7 @@ usn=:usn, mod=:mod, factor=:fact where id=:id and odid=0 and queue >=0""", self.col.db.execute( "update cards set reps=0, lapses=0 where id in " + ids2str(nonNew)) self.forgetCards(nonNew) + self.col.log(ids) # Repositioning new cards ########################################################################## @@ -1370,6 +1394,7 @@ and due >= ? and queue = 0""" % scids, now, self.col.usn(), shiftby, low) d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id)) self.col.db.executemany( "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d) + self.col.log(cids) def randomizeCards(self, did): cids = self.col.db.list("select id from cards where did = ?", did) diff --git a/anki/stats.py b/anki/stats.py index 22eb3a728..8171a4ae6 100644 --- a/anki/stats.py +++ b/anki/stats.py @@ -3,11 +3,15 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import division -import time, datetime, json +import time +import datetime +import json + import anki.js from anki.utils import fmtTimeSpan, ids2str from anki.lang import _, ngettext + # Card stats ########################################################################## @@ -56,6 +60,8 @@ class CardStats(object): self.addLine(_("Card Type"), c.template()['name']) self.addLine(_("Note Type"), c.model()['name']) self.addLine(_("Deck"), self.col.decks.name(c.did)) + self.addLine(_("Note ID"), c.nid) + self.addLine(_("Card ID"), c.id) self.txt += "" return self.txt diff --git a/anki/sync.py b/anki/sync.py index e8a53a002..d85306338 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -17,9 +17,12 @@ from hooks import runHook import anki # syncing vars -HTTP_TIMEOUT = 30 +HTTP_TIMEOUT = 90 HTTP_PROXY = None +# badly named; means no retries, and doesn't affect ssl connections +httplib2.RETRIES = 1 + try: # httplib2 >=0.7.7 _proxy_info_from_environment = httplib2.proxy_info_from_environment @@ -105,6 +108,7 @@ class Syncer(object): # step 1: login & metadata runHook("sync", "login") meta = self.server.meta() + self.col.log("rmeta", meta) if not meta: return "badAuth" rscm = meta['scm'] @@ -125,19 +129,24 @@ class Syncer(object): # and require confirmation if it's non-empty pass meta = self.meta() + self.col.log("lmeta", meta) self.lmod = meta['mod'] self.minUsn = meta['usn'] lscm = meta['scm'] lts = meta['ts'] if abs(rts - lts) > 300: + self.col.log("clock off") return "clockOff" if self.lmod == self.rmod: + self.col.log("no changes") return "noChanges" elif lscm != rscm: + self.col.log("schema diff") return "fullSync" self.lnewer = self.lmod > self.rmod # step 1.5: check collection is valid if not self.col.basicCheck(): + self.col.log("basic check") return "basicCheckFailed" # step 2: deletions runHook("sync", "meta") @@ -154,6 +163,7 @@ class Syncer(object): while 1: runHook("sync", "stream") chunk = self.server.chunk() + self.col.log("server chunk", chunk) self.applyChunk(chunk=chunk) if chunk['done']: break @@ -162,6 +172,7 @@ class Syncer(object): while 1: runHook("sync", "stream") chunk = self.chunk() + self.col.log("client chunk", chunk) self.server.applyChunk(chunk=chunk) if chunk['done']: break @@ -478,6 +489,7 @@ from notes where %s""" % d) for r in data: if r[0] not in lmods or lmods[r[0]] < r[modIdx]: update.append(r) + self.col.log(table, data) return update def mergeCards(self, cards): diff --git a/aqt/browser.py b/aqt/browser.py index 91c4b5644..defe4fce9 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -45,7 +45,7 @@ class DataModel(QAbstractTableModel): def getCard(self, index): id = self.cards[index.row()] if not id in self.cardObjs: - self.cardObjs[id] = self.col.getCard(id) + self.cardObjs[id] = self.col.getCard(id, log=False) return self.cardObjs[id] def refreshNote(self, note): diff --git a/aqt/deckconf.py b/aqt/deckconf.py index 5e4e442f6..3404bc703 100644 --- a/aqt/deckconf.py +++ b/aqt/deckconf.py @@ -1,13 +1,13 @@ # Copyright: Damien Elmes # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from anki.consts import NEW_CARDS_RANDOM +from operator import itemgetter +from anki.consts import NEW_CARDS_RANDOM from aqt.qt import * import aqt from aqt.utils import showInfo, showWarning, openHelp, getOnlyText, askUser, \ tooltip -from operator import itemgetter class DeckConf(QDialog): def __init__(self, mw, deck): @@ -189,6 +189,7 @@ class DeckConf(QDialog): f.fi1.setValue(c['ivlFct']*100) f.maxIvl.setValue(c['maxIvl']) f.revplim.setText(self.parentLimText('rev')) + f.buryRev.setChecked(c.get("bury", True)) # lapse c = self.conf['lapse'] f.lapSteps.setText(self.listToUser(c['delays'])) @@ -270,6 +271,7 @@ class DeckConf(QDialog): c['ease4'] = f.easyBonus.value()/100.0 c['ivlFct'] = f.fi1.value()/100.0 c['maxIvl'] = f.maxIvl.value() + c['bury'] = f.buryRev.isChecked() # lapse c = self.conf['lapse'] self.updateList(c, 'delays', f.lapSteps, minSize=0) diff --git a/aqt/main.py b/aqt/main.py index 3d703aa6b..3644df94e 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -2,20 +2,30 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import os, sys, re, traceback, signal +import os +import pprint +import sys +import re +import traceback +import signal import zipfile + from send2trash import send2trash from aqt.qt import * - from anki import Collection from anki.utils import isWin, isMac, intTime, splitFields, ids2str -from anki.hooks import runHook, addHook -import aqt, aqt.progress, aqt.webview, aqt.toolbar, aqt.stats +from anki.hooks import runHook, addHook +import aqt +import aqt.progress +import aqt.webview +import aqt.toolbar +import aqt.stats from aqt.utils import restoreGeom, showInfo, showWarning,\ restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \ openHelp, openLink, checkInvalidFilename + class AnkiQt(QMainWindow): def __init__(self, app, profileManager, args): QMainWindow.__init__(self) @@ -159,7 +169,6 @@ class AnkiQt(QMainWindow): return True def profileNameOk(self, str): - from anki.utils import invalidFilename, invalidFilenameChars return not checkInvalidFilename(str) def onAddProfile(self): @@ -261,7 +270,8 @@ To import into a password protected profile, please open the profile before atte showWarning("""\ Your collection is corrupt. Please see the manual for \ how to restore from a backup.""") - return self.unloadProfile() + self.unloadProfile() + raise self.hideSchemaMsg = False self.progress.setupDB(self.col.db) self.maybeEnableUndo() @@ -829,6 +839,7 @@ the problem and restart Anki.""") def setupHooks(self): addHook("modSchema", self.onSchemaMod) addHook("remNotes", self.onRemNotes) + addHook("log", self.onLog) # Log note deletion ########################################################################## @@ -846,6 +857,23 @@ the problem and restart Anki.""") f.write(("\t".join([str(id), str(mid)] + fields)).encode("utf8")) f.write("\n") + # Debug logging + ########################################################################## + + def onLog(self, args, kwargs): + def customRepr(x): + if isinstance(x, basestring): + return x + return pprint.pformat(x) + path, num, fn, y = traceback.extract_stack( + limit=4+kwargs.get("stack", 0))[0] + buf = u"[%s] %s:%s(): %s" % (intTime(), os.path.basename(path), fn, + ", ".join([customRepr(x) for x in args])) + lpath = re.sub("\.anki2$", ".log", self.pm.collectionPath()) + open(lpath, "ab").write(buf.encode("utf8") + "\n") + if os.environ.get("LOG"): + print buf + # Schema modifications ########################################################################## @@ -1051,6 +1079,8 @@ will be lost. Continue?""")) elif isWin: # make sure ctypes is bundled from ctypes import windll, wintypes + _dummy = windll + _dummy = wintypes def maybeHideAccelerators(self, tgt=None): if not self.hideMenuAccels: @@ -1076,6 +1106,10 @@ will be lost. Continue?""")) self.connect(self.app, SIGNAL("appMsg"), self.onAppMsg) def onAppMsg(self, buf): + if not isinstance(buf, unicode): + # even though we're sending this as unicode up above, + # a bug report still came in that we were receiving a qbytearray + buf = unicode(buf, "utf8", "ignore") if self.state == "startup": # try again in a second return self.progress.timer(1000, lambda: self.onAppMsg(buf), False) diff --git a/designer/dconf.ui b/designer/dconf.ui index 11d6b0160..2896abd80 100644 --- a/designer/dconf.ui +++ b/designer/dconf.ui @@ -271,13 +271,6 @@ - - - - - - - @@ -292,25 +285,6 @@ - - - - 0 - - - 0.000000000000000 - - - 999.000000000000000 - - - 1.000000000000000 - - - 100.000000000000000 - - - @@ -335,6 +309,39 @@ + + + + + + + + + + + 0 + + + 0.000000000000000 + + + 999.000000000000000 + + + 1.000000000000000 + + + 100.000000000000000 + + + + + + + Bury related reviews until the next day + + + @@ -616,6 +623,7 @@ easyBonus fi1 maxIvl + buryRev lapSteps lapMult lapMinInt