This commit is contained in:
Soren I. Bjornstad 2013-10-19 10:41:44 -05:00
commit 367a961bba
12 changed files with 162 additions and 51 deletions

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# 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)

View file

@ -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)

View file

@ -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,

View file

@ -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):

View file

@ -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):

View file

@ -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)

View file

@ -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 += "</table>"
return self.txt

View file

@ -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):

View file

@ -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):

View file

@ -1,13 +1,13 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# -*- 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)

View file

@ -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)

View file

@ -271,13 +271,6 @@
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="revplim">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_33">
<property name="text">
@ -292,25 +285,6 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="fi1">
<property name="decimals">
<number>0</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>999.000000000000000</double>
</property>
<property name="singleStep">
<double>1.000000000000000</double>
</property>
<property name="value">
<double>100.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
@ -335,6 +309,39 @@
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="revplim">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="fi1">
<property name="decimals">
<number>0</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>999.000000000000000</double>
</property>
<property name="singleStep">
<double>1.000000000000000</double>
</property>
<property name="value">
<double>100.000000000000000</double>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QCheckBox" name="buryRev">
<property name="text">
<string>Bury related reviews until the next day</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
@ -616,6 +623,7 @@
<tabstop>easyBonus</tabstop>
<tabstop>fi1</tabstop>
<tabstop>maxIvl</tabstop>
<tabstop>buryRev</tabstop>
<tabstop>lapSteps</tabstop>
<tabstop>lapMult</tabstop>
<tabstop>lapMinInt</tabstop>