deck -> collection

This commit is contained in:
Damien Elmes 2011-11-23 17:47:44 +09:00
parent 6e4e8249fb
commit 279a942642
32 changed files with 523 additions and 526 deletions

View file

@ -3,15 +3,15 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""\ """\
Open a deck: Open a collection:
deck = anki.Deck(path) col = anki.open(path)
Get a due card: Get a due card:
card = deck.sched.getCard() card = col.sched.getCard()
if not card: if not card:
# deck is finished # current deck is finished
Show the card: Show the card:
@ -19,11 +19,11 @@ Show the card:
Answer the card: Answer the card:
deck.sched.answerCard(card, ease) col.sched.answerCard(card, ease)
Refresh after a change: Refresh after a change:
deck.reset() col.reset()
Edit the card: Edit the card:
@ -34,7 +34,7 @@ Edit the card:
Save & close: Save & close:
deck.close() col.close()
""" """
import sys import sys
@ -50,4 +50,5 @@ if not os.path.exists(os.path.expanduser("~/.no-warranty")):
raise Exception("Don't use this without reading the forum thread") raise Exception("Don't use this without reading the forum thread")
version = "1.99" version = "1.99"
from anki.storage import Deck from anki.storage import Collection
open = Collection

View file

@ -18,8 +18,8 @@ from anki.utils import intTime, hexifyID, timestampID
class Card(object): class Card(object):
def __init__(self, deck, id=None): def __init__(self, col, id=None):
self.deck = deck self.col = col
self.timerStarted = None self.timerStarted = None
self._qa = None self._qa = None
self._rd = None self._rd = None
@ -28,7 +28,7 @@ class Card(object):
self.load() self.load()
else: else:
# to flush, set nid, ord, and due # to flush, set nid, ord, and due
self.id = timestampID(deck.db, "cards") self.id = timestampID(col.db, "cards")
self.gid = 1 self.gid = 1
self.crt = intTime() self.crt = intTime()
self.type = 0 self.type = 0
@ -59,15 +59,15 @@ class Card(object):
self.left, self.left,
self.edue, self.edue,
self.flags, self.flags,
self.data) = self.deck.db.first( self.data) = self.col.db.first(
"select * from cards where id = ?", self.id) "select * from cards where id = ?", self.id)
self._qa = None self._qa = None
self._rd = None self._rd = None
def flush(self): def flush(self):
self.mod = intTime() self.mod = intTime()
self.usn = self.deck.usn() self.usn = self.col.usn()
self.deck.db.execute( self.col.db.execute(
""" """
insert or replace into cards values insert or replace into cards values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
@ -91,8 +91,8 @@ insert or replace into cards values
def flushSched(self): def flushSched(self):
self.mod = intTime() self.mod = intTime()
self.usn = self.deck.usn() self.usn = self.col.usn()
self.deck.db.execute( self.col.db.execute(
"""update cards set """update cards set
mod=?, usn=?, type=?, queue=?, due=?, ivl=?, factor=?, reps=?, mod=?, usn=?, type=?, queue=?, due=?, ivl=?, factor=?, reps=?,
lapses=?, left=?, edue=? where id = ?""", lapses=?, left=?, edue=? where id = ?""",
@ -111,14 +111,14 @@ lapses=?, left=?, edue=? where id = ?""",
f = self.note(); m = self.model() f = self.note(); m = self.model()
data = [self.id, f.id, m['id'], self.gid, self.ord, f.stringTags(), data = [self.id, f.id, m['id'], self.gid, self.ord, f.stringTags(),
f.joinedFields()] f.joinedFields()]
self._qa = self.deck._renderQA(data) self._qa = self.col._renderQA(data)
return self._qa return self._qa
def _reviewData(self, reload=False): def _reviewData(self, reload=False):
"Fetch the model and note." "Fetch the model and note."
if not self._rd or reload: if not self._rd or reload:
f = self.deck.getNote(self.nid) f = self.col.getNote(self.nid)
m = self.deck.models.get(f.mid) m = self.col.models.get(f.mid)
self._rd = [f, m] self._rd = [f, m]
return self._rd return self._rd
@ -129,7 +129,7 @@ lapses=?, left=?, edue=? where id = ?""",
return self._reviewData()[1] return self._reviewData()[1]
def groupConf(self): def groupConf(self):
return self.deck.groups.conf(self.gid) return self.col.groups.conf(self.gid)
def template(self): def template(self):
return self._reviewData()[1]['tmpls'][self.ord] return self._reviewData()[1]['tmpls'][self.ord]

View file

@ -33,8 +33,8 @@ defaultConf = {
'sortBackwards': False, 'sortBackwards': False,
} }
# this is initialized by storage.Deck # this is initialized by storage.Collection
class _Deck(object): class _Collection(object):
def __init__(self, db, server=False): def __init__(self, db, server=False):
self.db = db self.db = db
@ -82,7 +82,7 @@ class _Deck(object):
gconf, gconf,
tags) = self.db.first(""" tags) = self.db.first("""
select crt, mod, scm, dty, usn, ls, select crt, mod, scm, dty, usn, ls,
conf, models, groups, gconf, tags from deck""") conf, models, groups, gconf, tags from col""")
self.conf = simplejson.loads(self.conf) self.conf = simplejson.loads(self.conf)
self.models.load(models) self.models.load(models)
self.groups.load(groups, gconf) self.groups.load(groups, gconf)
@ -92,7 +92,7 @@ conf, models, groups, gconf, tags from deck""")
"Flush state to DB, updating mod time." "Flush state to DB, updating mod time."
self.mod = intTime(1000) if mod is None else mod self.mod = intTime(1000) if mod is None else mod
self.db.execute( self.db.execute(
"""update deck set """update col set
crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.crt, self.mod, self.scm, self.dty, self.crt, self.mod, self.scm, self.dty,
self._usn, self.ls, simplejson.dumps(self.conf)) self._usn, self.ls, simplejson.dumps(self.conf))
@ -114,7 +114,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.save() self.save()
def lock(self): def lock(self):
self.db.execute("update deck set mod=mod") self.db.execute("update col set mod=mod")
def close(self, save=True): def close(self, save=True):
"Disconnect from DB." "Disconnect from DB."
@ -229,7 +229,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
return anki.notes.Note(self, self.models.current()) return anki.notes.Note(self, self.models.current())
def addNote(self, note): def addNote(self, note):
"Add a note to the deck. Return number of new cards." "Add a note to the collection. Return number of new cards."
# check we have card models available, then save # check we have card models available, then save
cms = self.findTemplates(note) cms = self.findTemplates(note)
if not cms: if not cms:
@ -464,8 +464,8 @@ where c.nid == f.id
return CardStats(self, card).report() return CardStats(self, card).report()
def stats(self): def stats(self):
from anki.stats import DeckStats from anki.stats import CollectionStats
return DeckStats(self) return CollectionStats(self)
# Timeboxing # Timeboxing
########################################################################## ##########################################################################

View file

@ -10,8 +10,8 @@ from anki.sched import Scheduler
class CramScheduler(Scheduler): class CramScheduler(Scheduler):
name = "cram" name = "cram"
def __init__(self, deck, order, min=0, max=None): def __init__(self, col, order, min=0, max=None):
Scheduler.__init__(self, deck) Scheduler.__init__(self, col)
# should be the opposite order of what you want # should be the opposite order of what you want
self.order = order self.order = order
# days to limit cram to, where tomorrow=0. Max is inclusive. # days to limit cram to, where tomorrow=0. Max is inclusive.
@ -65,7 +65,7 @@ class CramScheduler(Scheduler):
maxlim = "and due <= %d" % (self.today+1+self.max) maxlim = "and due <= %d" % (self.today+1+self.max)
else: else:
maxlim = "" maxlim = ""
self.newQueue = self.deck.db.list(""" self.newQueue = self.col.db.list("""
select id from cards where gid in %s and queue = 2 and due >= %d select id from cards where gid in %s and queue = 2 and due >= %d
%s order by %s limit %d""" % (self._groupLimit(), %s order by %s limit %d""" % (self._groupLimit(),
self.today+1+self.min, self.today+1+self.min,
@ -95,7 +95,7 @@ select id from cards where gid in %s and queue = 2 and due >= %d
ivl = self._graduatingIvl(card, conf, early) ivl = self._graduatingIvl(card, conf, early)
card.due = self.today + ivl card.due = self.today + ivl
# temporarily suspend it # temporarily suspend it
self.deck.setDirty() self.col.setDirty()
card.queue = -3 card.queue = -3
def _graduatingIvl(self, card, conf, early): def _graduatingIvl(self, card, conf, early):

View file

@ -4,15 +4,15 @@
import itertools, time, re, os, HTMLParser import itertools, time, re, os, HTMLParser
from operator import itemgetter from operator import itemgetter
from anki import Deck #from anki import Deck
from anki.cards import Card from anki.cards import Card
from anki.sync import SyncClient, SyncServer, copyLocalMedia from anki.sync import SyncClient, SyncServer, copyLocalMedia
from anki.lang import _ from anki.lang import _
from anki.utils import parseTags, stripHTML, ids2str from anki.utils import parseTags, stripHTML, ids2str
class Exporter(object): class Exporter(object):
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
self.limitTags = [] self.limitTags = []
self.limitCardIds = [] self.limitCardIds = []
@ -45,10 +45,10 @@ class Exporter(object):
if self.limitCardIds: if self.limitCardIds:
return self.limitCardIds return self.limitCardIds
if not self.limitTags: if not self.limitTags:
cards = self.deck.db.column0("select id from cards") cards = self.col.db.column0("select id from cards")
else: else:
d = tagIds(self.deck.db, self.limitTags, create=False) d = tagIds(self.col.db, self.limitTags, create=False)
cards = self.deck.db.column0( cards = self.col.db.column0(
"select cardId from cardTags where tagid in %s" % "select cardId from cardTags where tagid in %s" %
ids2str(d.values())) ids2str(d.values()))
self.count = len(cards) self.count = len(cards)
@ -56,11 +56,11 @@ class Exporter(object):
class AnkiExporter(Exporter): class AnkiExporter(Exporter):
key = _("Anki Deck (*.anki)") key = _("Anki Collection (*.anki)")
ext = ".anki" ext = ".anki"
def __init__(self, deck): def __init__(self, col):
Exporter.__init__(self, deck) Exporter.__init__(self, col)
self.includeSchedulingInfo = False self.includeSchedulingInfo = False
self.includeMedia = True self.includeMedia = True
@ -72,7 +72,7 @@ class AnkiExporter(Exporter):
os.unlink(path) os.unlink(path)
except (IOError, OSError): except (IOError, OSError):
pass pass
self.newDeck = DeckStorage.Deck(path) self.newCol = DeckStorage.Deck(path)
client = SyncClient(self.deck) client = SyncClient(self.deck)
server = SyncServer(self.newDeck) server = SyncServer(self.newDeck)
client.setServer(server) client.setServer(server)

View file

@ -18,10 +18,10 @@ SEARCH_GROUP = 7
# Tools # Tools
########################################################################## ##########################################################################
def fieldNames(deck, downcase=True): def fieldNames(col, downcase=True):
fields = set() fields = set()
names = [] names = []
for m in deck.models.all(): for m in col.models.all():
for f in m['flds']: for f in m['flds']:
if f['name'].lower() not in fields: if f['name'].lower() not in fields:
names.append(f['name']) names.append(f['name'])
@ -35,8 +35,8 @@ def fieldNames(deck, downcase=True):
class Finder(object): class Finder(object):
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
def findCards(self, query, full=False): def findCards(self, query, full=False):
"Return a list of card ids for QUERY." "Return a list of card ids for QUERY."
@ -47,8 +47,8 @@ class Finder(object):
return [] return []
(q, args) = self._whereClause() (q, args) = self._whereClause()
query = self._orderedSelect(q) query = self._orderedSelect(q)
res = self.deck.db.list(query, **args) res = self.col.db.list(query, **args)
if self.deck.conf['sortBackwards']: if self.col.conf['sortBackwards']:
res.reverse() res.reverse()
return res return res
@ -65,7 +65,7 @@ class Finder(object):
return q, self.lims['args'] return q, self.lims['args']
def _orderedSelect(self, lim): def _orderedSelect(self, lim):
type = self.deck.conf['sortType'] type = self.col.conf['sortType']
if not type: if not type:
return "select id from cards c where " + lim return "select id from cards c where " + lim
elif type.startswith("note"): elif type.startswith("note"):
@ -153,7 +153,7 @@ order by %s""" % (lim, sort)
elif val == "suspended": elif val == "suspended":
cond = "queue = -1" cond = "queue = -1"
elif val == "due": elif val == "due":
cond = "(queue = 2 and due <= %d)" % self.deck.sched.today cond = "(queue = 2 and due <= %d)" % self.col.sched.today
elif val == "recent": elif val == "recent":
cond = "c.id in (select id from cards order by mod desc limit 100)" cond = "c.id in (select id from cards order by mod desc limit 100)"
if neg: if neg:
@ -174,7 +174,7 @@ order by %s""" % (lim, sort)
# in the future we may want to apply this at the end to speed up # in the future we may want to apply this at the end to speed up
# the case where there are other limits # the case where there are other limits
nids = [] nids = []
for nid, flds in self.deck.db.execute( for nid, flds in self.col.db.execute(
"select id, flds from notes"): "select id, flds from notes"):
if val in stripHTML(flds): if val in stripHTML(flds):
nids.append(nid) nids.append(nid)
@ -186,14 +186,14 @@ order by %s""" % (lim, sort)
def _findModel(self, val, isNeg): def _findModel(self, val, isNeg):
extra = "not" if isNeg else "" extra = "not" if isNeg else ""
ids = [] ids = []
for m in self.deck.models.all(): for m in self.col.models.all():
if m['name'].lower() == val: if m['name'].lower() == val:
ids.append(m['id']) ids.append(m['id'])
self.lims['note'].append("mid %s in %s" % (extra, ids2str(ids))) self.lims['note'].append("mid %s in %s" % (extra, ids2str(ids)))
def _findGroup(self, val, isNeg): def _findGroup(self, val, isNeg):
extra = "!" if isNeg else "" extra = "!" if isNeg else ""
id = self.deck.groups.id(val, create=False) or 0 id = self.col.groups.id(val, create=False) or 0
self.lims['card'].append("c.gid %s= %s" % (extra, id)) self.lims['card'].append("c.gid %s= %s" % (extra, id))
def _findTemplate(self, val, isNeg): def _findTemplate(self, val, isNeg):
@ -205,7 +205,7 @@ order by %s""" % (lim, sort)
except: except:
num = None num = None
lims = [] lims = []
for m in self.deck.models.all(): for m in self.col.models.all():
for t in m['tmpls']: for t in m['tmpls']:
# ordinal number? # ordinal number?
if num is not None and t['ord'] == num: if num is not None and t['ord'] == num:
@ -228,7 +228,7 @@ order by %s""" % (lim, sort)
value = "%" + parts[1].replace("*", "%") + "%" value = "%" + parts[1].replace("*", "%") + "%"
# find models that have that field # find models that have that field
mods = {} mods = {}
for m in self.deck.models.all(): for m in self.col.models.all():
for f in m['flds']: for f in m['flds']:
if f['name'].lower() == field: if f['name'].lower() == field:
mods[m['id']] = (m, f['ord']) mods[m['id']] = (m, f['ord'])
@ -239,7 +239,7 @@ order by %s""" % (lim, sort)
# gather nids # gather nids
regex = value.replace("%", ".*") regex = value.replace("%", ".*")
nids = [] nids = []
for (id,mid,flds) in self.deck.db.execute(""" for (id,mid,flds) in self.col.db.execute("""
select id, mid, flds from notes select id, mid, flds from notes
where mid in %s and flds like ? escape '\\'""" % ( where mid in %s and flds like ? escape '\\'""" % (
ids2str(mods.keys())), ids2str(mods.keys())),
@ -258,7 +258,7 @@ where mid in %s and flds like ? escape '\\'""" % (
def _parseQuery(self): def _parseQuery(self):
tokens = [] tokens = []
res = [] res = []
allowedfields = fieldNames(self.deck) allowedfields = fieldNames(self.col)
def addSearchFieldToken(field, value, isNeg): def addSearchFieldToken(field, value, isNeg):
if field.lower() in allowedfields: if field.lower() in allowedfields:
res.append((field + ':' + value, isNeg, SEARCH_FIELD)) res.append((field + ':' + value, isNeg, SEARCH_FIELD))
@ -370,11 +370,11 @@ where mid in %s and flds like ? escape '\\'""" % (
# Find and replace # Find and replace
########################################################################## ##########################################################################
def findReplace(deck, nids, src, dst, regex=False, field=None, fold=True): def findReplace(col, nids, src, dst, regex=False, field=None, fold=True):
"Find and replace fields in a note." "Find and replace fields in a note."
mmap = {} mmap = {}
if field: if field:
for m in deck.models.all(): for m in col.models.all():
for f in m['flds']: for f in m['flds']:
if f['name'] == field: if f['name'] == field:
mmap[m['id']] = f['ord'] mmap[m['id']] = f['ord']
@ -389,7 +389,7 @@ def findReplace(deck, nids, src, dst, regex=False, field=None, fold=True):
def repl(str): def repl(str):
return re.sub(regex, dst, str) return re.sub(regex, dst, str)
d = [] d = []
for nid, mid, flds in deck.db.execute( for nid, mid, flds in col.db.execute(
"select id, mid, flds from notes where id in "+ids2str(nids)): "select id, mid, flds from notes where id in "+ids2str(nids)):
origFlds = flds origFlds = flds
# does it match? # does it match?
@ -402,19 +402,19 @@ def findReplace(deck, nids, src, dst, regex=False, field=None, fold=True):
sflds[c] = repl(sflds[c]) sflds[c] = repl(sflds[c])
flds = joinFields(sflds) flds = joinFields(sflds)
if flds != origFlds: if flds != origFlds:
d.append(dict(nid=nid,flds=flds,u=deck.usn(),m=intTime())) d.append(dict(nid=nid,flds=flds,u=col.usn(),m=intTime()))
if not d: if not d:
return 0 return 0
# replace # replace
deck.db.executemany("update notes set flds=:flds,mod=:m,usn=:u where id=:nid", d) col.db.executemany("update notes set flds=:flds,mod=:m,usn=:u where id=:nid", d)
deck.updateFieldCache(nids) col.updateFieldCache(nids)
return len(d) return len(d)
# Find duplicates # Find duplicates
########################################################################## ##########################################################################
def findDuplicates(deck, fmids): def findDuplicates(col, fmids):
data = deck.db.all( data = col.db.all(
"select nid, value from fdata where fmid in %s" % "select nid, value from fdata where fmid in %s" %
ids2str(fmids)) ids2str(fmids))
vals = {} vals = {}

View file

@ -83,8 +83,8 @@ class GroupManager(object):
# Registry save/load # Registry save/load
############################################################# #############################################################
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
def load(self, groups, gconf): def load(self, groups, gconf):
self.groups = simplejson.loads(groups) self.groups = simplejson.loads(groups)
@ -95,12 +95,12 @@ class GroupManager(object):
"Can be called with either a group or a group configuration." "Can be called with either a group or a group configuration."
if g: if g:
g['mod'] = intTime() g['mod'] = intTime()
g['usn'] = self.deck.usn() g['usn'] = self.col.usn()
self.changed = True self.changed = True
def flush(self): def flush(self):
if self.changed: if self.changed:
self.deck.db.execute("update deck set groups=?, gconf=?", self.col.db.execute("update col set groups=?, gconf=?",
simplejson.dumps(self.groups), simplejson.dumps(self.groups),
simplejson.dumps(self.gconf)) simplejson.dumps(self.gconf))
@ -144,10 +144,10 @@ class GroupManager(object):
self.rem(id, cardsToo) self.rem(id, cardsToo)
# delete cards too? # delete cards too?
if cardsToo: if cardsToo:
self.deck.remCards(self.cids(gid)) self.col.remCards(self.cids(gid))
# delete the group and add a grave # delete the group and add a grave
del self.groups[str(gid)] del self.groups[str(gid)]
self.deck._logRem([gid], REM_GROUP) self.col._logRem([gid], REM_GROUP)
# ensure we have an active group # ensure we have an active group
if gid in self.active(): if gid in self.active():
self.select(int(self.groups.keys()[0])) self.select(int(self.groups.keys()[0]))
@ -239,7 +239,7 @@ class GroupManager(object):
def remConf(self, id): def remConf(self, id):
"Remove a configuration and update all groups using it." "Remove a configuration and update all groups using it."
assert int(id) != 1 assert int(id) != 1
self.deck.modSchema() self.col.modSchema()
del self.gconf[str(id)] del self.gconf[str(id)]
for g in self.all(): for g in self.all():
if str(g['conf']) == str(id): if str(g['conf']) == str(id):
@ -257,9 +257,9 @@ class GroupManager(object):
return self.get(gid)['name'] return self.get(gid)['name']
def setGroup(self, cids, gid): def setGroup(self, cids, gid):
self.deck.db.execute( self.col.db.execute(
"update cards set gid=?,usn=?,mod=? where id in "+ "update cards set gid=?,usn=?,mod=? where id in "+
ids2str(cids), gid, self.deck.usn(), intTime()) ids2str(cids), gid, self.col.usn(), intTime())
def maybeAddToActive(self): def maybeAddToActive(self):
@ -267,29 +267,29 @@ class GroupManager(object):
self.select(self.selected()) self.select(self.selected())
def sendHome(self, cids): def sendHome(self, cids):
self.deck.db.execute(""" self.col.db.execute("""
update cards set gid=(select gid from notes f where f.id=nid), update cards set gid=(select gid from notes f where f.id=nid),
usn=?,mod=? where id in %s""" % ids2str(cids), usn=?,mod=? where id in %s""" % ids2str(cids),
self.deck.usn(), intTime(), gid) self.col.usn(), intTime(), gid)
def cids(self, gid): def cids(self, gid):
return self.deck.db.list("select id from cards where gid=?", gid) return self.col.db.list("select id from cards where gid=?", gid)
# Group selection # Group selection
############################################################# #############################################################
def top(self): def top(self):
"The current top level group as an object." "The current top level group as an object."
g = self.get(self.deck.conf['topGroup']) g = self.get(self.col.conf['topGroup'])
return g return g
def active(self): def active(self):
"The currrently active gids." "The currrently active gids."
return self.deck.conf['activeGroups'] return self.col.conf['activeGroups']
def selected(self): def selected(self):
"The currently selected gid." "The currently selected gid."
return self.deck.conf['curGroup'] return self.col.conf['curGroup']
def current(self): def current(self):
return self.get(self.selected()) return self.get(self.selected())
@ -298,13 +298,13 @@ usn=?,mod=? where id in %s""" % ids2str(cids),
"Select a new branch." "Select a new branch."
# save the top level group # save the top level group
name = self.groups[str(gid)]['name'] name = self.groups[str(gid)]['name']
self.deck.conf['topGroup'] = self._topFor(name) self.col.conf['topGroup'] = self._topFor(name)
# current group # current group
self.deck.conf['curGroup'] = gid self.col.conf['curGroup'] = gid
# and active groups (current + all children) # and active groups (current + all children)
actv = self.children(gid) actv = self.children(gid)
actv.sort() actv.sort()
self.deck.conf['activeGroups'] = [gid] + [a[1] for a in actv] self.col.conf['activeGroups'] = [gid] + [a[1] for a in actv]
def children(self, gid): def children(self, gid):
"All children of gid, as (name, id)." "All children of gid, as (name, id)."

View file

@ -10,7 +10,7 @@ from anki.lang import _
Importers = ( Importers = (
(_("Text separated by tabs or semicolons (*.txt,*.csv)"), TextImporter), (_("Text separated by tabs or semicolons (*.txt,*.csv)"), TextImporter),
(_("Anki 2.0 Deck (*.anki2)"), Anki2Importer), (_("Anki 2.0 Collection (*.anki2)"), Anki2Importer),
(_("Anki 1.2 Deck (*.anki)"), Anki1Importer), (_("Anki 1.2 Deck (*.anki)"), Anki1Importer),
(_("Supermemo XML export (*.xml)"), SupermemoXmlImporter), (_("Supermemo XML export (*.xml)"), SupermemoXmlImporter),
) )

View file

@ -2,13 +2,13 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from anki import Deck from anki import Collection
from anki.utils import intTime from anki.utils import intTime
from anki.importing.base import Importer from anki.importing.base import Importer
# #
# Import a .anki2 file into the current deck. Used for migration from 1.x, # Import a .anki2 file into the current collection. Used for migration from
# shared decks, and import from a packaged deck. # 1.x, shared decks, and import from a packaged deck.
# #
# We can't rely on internal ids, so we: # We can't rely on internal ids, so we:
# - compare notes by guid # - compare notes by guid
@ -24,7 +24,7 @@ class Anki2Importer(Importer):
needCards = True needCards = True
def run(self, media=None): def run(self, media=None):
self._prepareDecks() self._prepareFiles()
if media is not None: if media is not None:
# Anki1 importer has provided us with a custom media folder # Anki1 importer has provided us with a custom media folder
self.src.media._dir = media self.src.media._dir = media
@ -33,9 +33,9 @@ class Anki2Importer(Importer):
finally: finally:
self.src.close(save=False) self.src.close(save=False)
def _prepareDecks(self): def _prepareFiles(self):
self.dst = self.deck self.dst = self.col
self.src = Deck(self.file, queue=False) self.src = Collection(self.file, queue=False)
def _import(self): def _import(self):
self._groups = {} self._groups = {}
@ -61,7 +61,7 @@ class Anki2Importer(Importer):
for id, guid, mod, mid in self.dst.db.execute( for id, guid, mod, mid in self.dst.db.execute(
"select id, guid, mod, mid from notes"): "select id, guid, mod, mid from notes"):
self._notes[guid] = (id, mod, mid) self._notes[guid] = (id, mod, mid)
# iterate over source deck # iterate over source collection
add = [] add = []
dirty = [] dirty = []
for note in self.src.db.execute( for note in self.src.db.execute(
@ -69,7 +69,7 @@ class Anki2Importer(Importer):
# turn the db result into a mutable list # turn the db result into a mutable list
note = list(note) note = list(note)
guid, mid = note[1:3] guid, mid = note[1:3]
# missing from local deck? # missing from local col?
if guid not in self._notes: if guid not in self._notes:
# get corresponding local model # get corresponding local model
lmid = self._mid(mid) lmid = self._mid(mid)
@ -85,7 +85,7 @@ class Anki2Importer(Importer):
self._notes[guid] = (note[0], note[4], note[2]) self._notes[guid] = (note[0], note[4], note[2])
else: else:
continue #raise Exception("merging notes nyi") continue #raise Exception("merging notes nyi")
# add to deck # add to col
self.dst.db.executemany( self.dst.db.executemany(
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
add) add)
@ -140,7 +140,7 @@ class Anki2Importer(Importer):
###################################################################### ######################################################################
def _gid(self, gid): def _gid(self, gid):
"Given gid in src deck, return local id." "Given gid in src col, return local id."
# already converted? # already converted?
if gid in self._groups: if gid in self._groups:
return self._groups[gid] return self._groups[gid]
@ -179,7 +179,7 @@ class Anki2Importer(Importer):
"select f.guid, f.mid, c.* from cards c, notes f " "select f.guid, f.mid, c.* from cards c, notes f "
"where c.nid = f.id"): "where c.nid = f.id"):
guid = card[0] guid = card[0]
# does the card's note exist in dst deck? # does the card's note exist in dst col?
if guid not in self._notes: if guid not in self._notes:
continue continue
dnid = self._notes[guid] dnid = self._notes[guid]
@ -188,7 +188,7 @@ class Anki2Importer(Importer):
# mid = self._notes[guid][2] # mid = self._notes[guid][2]
# if shash != self._dstModels[mid]: # if shash != self._dstModels[mid]:
# continue # continue
# does the card already exist in the dst deck? # does the card already exist in the dst col?
ord = card[5] ord = card[5]
if (guid, ord) in self._cards: if (guid, ord) in self._cards:
# fixme: in future, could update if newer mod time # fixme: in future, could update if newer mod time

View file

@ -11,10 +11,10 @@ class Importer(object):
needMapper = False needMapper = False
def __init__(self, deck, file): def __init__(self, col, file):
self.file = file self.file = file
self.log = [] self.log = []
self.deck = deck self.col = col
self.total = 0 self.total = 0
def run(self): def run(self):

View file

@ -30,9 +30,9 @@ class CardImporter(Importer):
updateKey = None updateKey = None
needDelimiter = False needDelimiter = False
def __init__(self, deck, file): def __init__(self, col, file):
Importer.__init__(self, deck, file) Importer.__init__(self, col, file)
self._model = deck.currentModel self._model = col.currentModel
self.tagsToAdd = u"" self.tagsToAdd = u""
self._mapping = None self._mapping = None
@ -40,23 +40,23 @@ class CardImporter(Importer):
"Import." "Import."
if self.updateKey is not None: if self.updateKey is not None:
return self.doUpdate() return self.doUpdate()
random = self.deck.newCardOrder == NEW_CARDS_RANDOM random = self.col.newCardOrder == NEW_CARDS_RANDOM
num = 6 num = 6
if random: if random:
num += 1 num += 1
c = self.foreignCards() c = self.foreignCards()
if self.importCards(c): if self.importCards(c):
self.deck.updateCardTags(self.cardIds) self.col.updateCardTags(self.cardIds)
if random: if random:
self.deck.randomizeNewCards(self.cardIds) self.col.randomizeNewCards(self.cardIds)
if c: if c:
self.deck.setModified() self.col.setModified()
def doUpdate(self): def doUpdate(self):
# grab the data from the external file # grab the data from the external file
cards = self.foreignCards() cards = self.foreignCards()
# grab data from db # grab data from db
fields = self.deck.db.all(""" fields = self.col.db.all("""
select noteId, value from fields where fieldModelId = :id select noteId, value from fields where fieldModelId = :id
and value != ''""", and value != ''""",
id=self.updateKey[1]) id=self.updateKey[1])
@ -101,7 +101,7 @@ and value != ''""",
'v': c.fields[index], 'v': c.fields[index],
'chk': self.maybeChecksum(c.fields[index], fm.unique)} 'chk': self.maybeChecksum(c.fields[index], fm.unique)}
for (nid, c) in upcards] for (nid, c) in upcards]
self.deck.db.execute(""" self.col.db.execute("""
update fields set value = :v, chksum = :chk where noteId = :nid update fields set value = :v, chksum = :chk where noteId = :nid
and fieldModelId = :fmid""", data) and fieldModelId = :fmid""", data)
# update tags # update tags
@ -109,17 +109,17 @@ and fieldModelId = :fmid""", data)
data = [{'nid': nid, data = [{'nid': nid,
't': c.fields[tagsIdx]} 't': c.fields[tagsIdx]}
for (nid, c) in upcards] for (nid, c) in upcards]
self.deck.db.execute( self.col.db.execute(
"update notes set tags = :t where id = :nid", "update notes set tags = :t where id = :nid",
data) data)
# rebuild caches # rebuild caches
cids = self.deck.db.column0( cids = self.col.db.column0(
"select id from cards where noteId in %s" % "select id from cards where noteId in %s" %
ids2str(nids)) ids2str(nids))
self.deck.updateCardTags(cids) self.col.updateCardTags(cids)
self.deck.updateCardsFromNoteIds(nids) self.col.updateCardsFromNoteIds(nids)
self.total = len(cards) self.total = len(cards)
self.deck.setModified() self.col.setModified()
def fields(self): def fields(self):
"The number of fields." "The number of fields."
@ -166,7 +166,7 @@ and fieldModelId = :fmid""", data)
model = property(getModel, setModel) model = property(getModel, setModel)
def importCards(self, cards): def importCards(self, cards):
"Convert each card into a note, apply attributes and add to deck." "Convert each card into a note, apply attributes and add to col."
# ensure all unique and required fields are mapped # ensure all unique and required fields are mapped
for fm in self.model.fieldModels: for fm in self.model.fieldModels:
if fm.required or fm.unique: if fm.required or fm.unique:
@ -206,11 +206,11 @@ and fieldModelId = :fmid""", data)
d['created'] = tmp[0] d['created'] = tmp[0]
noteCreated[d['id']] = d['created'] noteCreated[d['id']] = d['created']
return d return d
self.deck.db.execute(notesTable.insert(), self.col.db.execute(notesTable.insert(),
[fudgeCreated({'modelId': self.model.id, [fudgeCreated({'modelId': self.model.id,
'tags': canonifyTags(self.tagsToAdd + " " + cards[n].tags), 'tags': canonifyTags(self.tagsToAdd + " " + cards[n].tags),
'id': noteIds[n]}) for n in range(len(cards))]) 'id': noteIds[n]}) for n in range(len(cards))])
self.deck.db.execute(""" self.col.db.execute("""
delete from notesDeleted delete from notesDeleted
where noteId in (%s)""" % ",".join([str(s) for s in noteIds])) where noteId in (%s)""" % ",".join([str(s) for s in noteIds]))
# add all the fields # add all the fields
@ -230,7 +230,7 @@ where noteId in (%s)""" % ",".join([str(s) for s in noteIds]))
cards[m].fields[index] or u"", fm.unique) cards[m].fields[index] or u"", fm.unique)
} }
for m in range(len(cards))] for m in range(len(cards))]
self.deck.db.execute(fieldsTable.insert(), self.col.db.execute(fieldsTable.insert(),
data) data)
# and cards # and cards
active = 0 active = 0
@ -246,9 +246,9 @@ where noteId in (%s)""" % ",".join([str(s) for s in noteIds]))
'question': u"", 'question': u"",
'answer': u"" 'answer': u""
},cards[m]) for m in range(len(cards))] },cards[m]) for m in range(len(cards))]
self.deck.db.execute(cardsTable.insert(), self.col.db.execute(cardsTable.insert(),
data) data)
self.deck.updateCardsFromNoteIds(noteIds) self.col.updateCardsFromNoteIds(noteIds)
self.total = len(noteIds) self.total = len(noteIds)
def addMeta(self, data, card): def addMeta(self, data, card):
@ -297,7 +297,7 @@ where noteId in (%s)""" % ",".join([str(s) for s in noteIds]))
def getUniqueCache(self, field): def getUniqueCache(self, field):
"Return a dict with all fields, to test for uniqueness." "Return a dict with all fields, to test for uniqueness."
return dict(self.deck.db.all( return dict(self.col.db.all(
"select value, 1 from fields where fieldModelId = :fmid", "select value, 1 from fields where fieldModelId = :fmid",
fmid=field.id)) fmid=field.id))

View file

@ -15,7 +15,7 @@ import re, unicodedata, time
#import chardet #import chardet
from anki import Deck #from anki import Deck
class SmartDict(dict): class SmartDict(dict):
""" """

View file

@ -30,22 +30,22 @@ def stripLatex(text):
text = text.replace(match.group(), "") text = text.replace(match.group(), "")
return text return text
def mungeQA(html, type, fields, model, data, deck): def mungeQA(html, type, fields, model, data, col):
"Convert TEXT with embedded latex tags to image links." "Convert TEXT with embedded latex tags to image links."
for match in regexps['standard'].finditer(html): for match in regexps['standard'].finditer(html):
html = html.replace(match.group(), _imgLink(deck, match.group(1), model)) html = html.replace(match.group(), _imgLink(col, match.group(1), model))
for match in regexps['expression'].finditer(html): for match in regexps['expression'].finditer(html):
html = html.replace(match.group(), _imgLink( html = html.replace(match.group(), _imgLink(
deck, "$" + match.group(1) + "$", model)) col, "$" + match.group(1) + "$", model))
for match in regexps['math'].finditer(html): for match in regexps['math'].finditer(html):
html = html.replace(match.group(), _imgLink( html = html.replace(match.group(), _imgLink(
deck, col,
"\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model)) "\\begin{displaymath}" + match.group(1) + "\\end{displaymath}", model))
return html return html
def _imgLink(deck, latex, model): def _imgLink(col, latex, model):
"Return an img link for LATEX, creating if necesssary." "Return an img link for LATEX, creating if necesssary."
txt = _latexFromHtml(deck, latex) txt = _latexFromHtml(col, latex)
fname = "latex-%s.png" % checksum(txt.encode("utf8")) fname = "latex-%s.png" % checksum(txt.encode("utf8"))
link = '<img src="%s">' % fname link = '<img src="%s">' % fname
if os.path.exists(fname): if os.path.exists(fname):
@ -53,13 +53,13 @@ def _imgLink(deck, latex, model):
elif not build: elif not build:
return u"[latex]%s[/latex]" % latex return u"[latex]%s[/latex]" % latex
else: else:
err = _buildImg(deck, txt, fname, model) err = _buildImg(col, txt, fname, model)
if err: if err:
return err return err
else: else:
return link return link
def _latexFromHtml(deck, latex): def _latexFromHtml(col, latex):
"Convert entities and fix newlines." "Convert entities and fix newlines."
for match in re.compile("&([a-z]+);", re.IGNORECASE).finditer(latex): for match in re.compile("&([a-z]+);", re.IGNORECASE).finditer(latex):
if match.group(1) in entitydefs: if match.group(1) in entitydefs:
@ -68,7 +68,7 @@ def _latexFromHtml(deck, latex):
latex = stripHTML(latex) latex = stripHTML(latex)
return latex return latex
def _buildImg(deck, latex, fname, model): def _buildImg(col, latex, fname, model):
# add header/footer & convert to utf8 # add header/footer & convert to utf8
latex = (model["latexPre"] + "\n" + latex = (model["latexPre"] + "\n" +
latex + "\n" + latex + "\n" +
@ -83,7 +83,7 @@ def _buildImg(deck, latex, fname, model):
texfile = file(namedtmp("tmp.tex"), "w") texfile = file(namedtmp("tmp.tex"), "w")
texfile.write(latex) texfile.write(latex)
texfile.close() texfile.close()
mdir = deck.media.dir() mdir = col.media.dir()
oldcwd = os.getcwd() oldcwd = os.getcwd()
png = namedtmp("tmp.png") png = namedtmp("tmp.png")
try: try:

View file

@ -17,10 +17,10 @@ class MediaManager(object):
regexps = ("(?i)(\[sound:([^]]+)\])", regexps = ("(?i)(\[sound:([^]]+)\])",
"(?i)(<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>)") "(?i)(<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>)")
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
# media directory # media directory
self._dir = re.sub("(?i)\.(anki2)$", ".media", self.deck.path) self._dir = re.sub("(?i)\.(anki2)$", ".media", self.col.path)
if not os.path.exists(self._dir): if not os.path.exists(self._dir):
os.makedirs(self._dir) os.makedirs(self._dir)
os.chdir(self._dir) os.chdir(self._dir)
@ -87,8 +87,8 @@ If the same name exists, compare checksums."""
def filesInStr(self, mid, string, includeRemote=False): def filesInStr(self, mid, string, includeRemote=False):
l = [] l = []
# convert latex first # convert latex first
model = self.deck.models.get(mid) model = self.col.models.get(mid)
string = mungeQA(string, None, None, model, None, self.deck) string = mungeQA(string, None, None, model, None, self.col)
# extract filenames # extract filenames
for reg in self.regexps: for reg in self.regexps:
for (full, fname) in re.findall(reg, string): for (full, fname) in re.findall(reg, string):
@ -161,7 +161,7 @@ If the same name exists, compare checksums."""
def allMedia(self): def allMedia(self):
"Return a set of all referenced filenames." "Return a set of all referenced filenames."
files = set() files = set()
for mid, flds in self.deck.db.execute("select mid, flds from notes"): for mid, flds in self.col.db.execute("select mid, flds from notes"):
for f in self.filesInStr(mid, flds): for f in self.filesInStr(mid, flds):
files.add(f) files.add(f)
return files return files

View file

@ -60,8 +60,8 @@ class ModelManager(object):
# Saving/loading registry # Saving/loading registry
############################################################# #############################################################
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
def load(self, json): def load(self, json):
"Load registry from JSON." "Load registry from JSON."
@ -72,16 +72,16 @@ class ModelManager(object):
"Mark M modified if provided, and schedule registry flush." "Mark M modified if provided, and schedule registry flush."
if m: if m:
m['mod'] = intTime() m['mod'] = intTime()
m['usn'] = self.deck.usn() m['usn'] = self.col.usn()
self._updateRequired(m) self._updateRequired(m)
if gencards: if gencards:
self.deck.genCards(self.nids(m)) self.col.genCards(self.nids(m))
self.changed = True self.changed = True
def flush(self): def flush(self):
"Flush the registry if any models were changed." "Flush the registry if any models were changed."
if self.changed: if self.changed:
self.deck.db.execute("update deck set models = ?", self.col.db.execute("update col set models = ?",
simplejson.dumps(self.models)) simplejson.dumps(self.models))
# Retrieving and creating models # Retrieving and creating models
@ -90,16 +90,16 @@ class ModelManager(object):
def current(self): def current(self):
"Get current model." "Get current model."
try: try:
m = self.get(self.deck.groups.top()['curModel']) m = self.get(self.col.groups.top()['curModel'])
assert m assert m
return m return m
except: except:
return self.models.values()[0] return self.models.values()[0]
def setCurrent(self, m): def setCurrent(self, m):
t = self.deck.groups.top() t = self.col.groups.top()
t['curModel'] = m['id'] t['curModel'] = m['id']
self.deck.groups.save(t) self.col.groups.save(t)
def get(self, id): def get(self, id):
"Get model with ID, or None." "Get model with ID, or None."
@ -130,10 +130,10 @@ class ModelManager(object):
def rem(self, m): def rem(self, m):
"Delete model, and all its cards/notes." "Delete model, and all its cards/notes."
self.deck.modSchema() self.col.modSchema()
current = self.current()['id'] == m['id'] current = self.current()['id'] == m['id']
# delete notes/cards # delete notes/cards
self.deck.remCards(self.deck.db.list(""" self.col.remCards(self.col.db.list("""
select id from cards where nid in (select id from notes where mid = ?)""", select id from cards where nid in (select id from notes where mid = ?)""",
m['id'])) m['id']))
# then the model # then the model
@ -170,12 +170,12 @@ select id from cards where nid in (select id from notes where mid = ?)""",
def nids(self, m): def nids(self, m):
"Note ids for M." "Note ids for M."
return self.deck.db.list( return self.col.db.list(
"select id from notes where mid = ?", m['id']) "select id from notes where mid = ?", m['id'])
def useCount(self, m): def useCount(self, m):
"Number of note using M." "Number of note using M."
return self.deck.db.scalar( return self.col.db.scalar(
"select count() from notes where mid = ?", m['id']) "select count() from notes where mid = ?", m['id'])
def randomNew(self): def randomNew(self):
@ -210,9 +210,9 @@ select id from cards where nid in (select id from notes where mid = ?)""",
def setSortIdx(self, m, idx): def setSortIdx(self, m, idx):
assert idx >= 0 and idx < len(m['flds']) assert idx >= 0 and idx < len(m['flds'])
self.deck.modSchema() self.col.modSchema()
m['sortf'] = idx m['sortf'] = idx
self.deck.updateFieldCache(self.nids(m), csum=False) self.col.updateFieldCache(self.nids(m), csum=False)
self.save(m) self.save(m)
def addField(self, m, field): def addField(self, m, field):
@ -234,7 +234,7 @@ select id from cards where nid in (select id from notes where mid = ?)""",
self._transformFields(m, delete) self._transformFields(m, delete)
if idx == self.sortIdx(m): if idx == self.sortIdx(m):
# need to rebuild # need to rebuild
self.deck.updateFieldCache(self.nids(m), csum=False) self.col.updateFieldCache(self.nids(m), csum=False)
# saves # saves
self.renameField(m, field, None) self.renameField(m, field, None)
@ -254,7 +254,7 @@ select id from cards where nid in (select id from notes where mid = ?)""",
self._transformFields(m, move) self._transformFields(m, move)
def renameField(self, m, field, newName): def renameField(self, m, field, newName):
self.deck.modSchema() self.col.modSchema()
for t in m['tmpls']: for t in m['tmpls']:
types = ("{{%s}}", "{{text:%s}}", "{{#%s}}", types = ("{{%s}}", "{{text:%s}}", "{{#%s}}",
"{{^%s}}", "{{/%s}}") "{{^%s}}", "{{/%s}}")
@ -273,13 +273,13 @@ select id from cards where nid in (select id from notes where mid = ?)""",
f['ord'] = c f['ord'] = c
def _transformFields(self, m, fn): def _transformFields(self, m, fn):
self.deck.modSchema() self.col.modSchema()
r = [] r = []
for (id, flds) in self.deck.db.execute( for (id, flds) in self.col.db.execute(
"select id, flds from notes where mid = ?", m['id']): "select id, flds from notes where mid = ?", m['id']):
r.append((joinFields(fn(splitFields(flds))), r.append((joinFields(fn(splitFields(flds))),
intTime(), self.deck.usn(), id)) intTime(), self.col.usn(), id))
self.deck.db.executemany( self.col.db.executemany(
"update notes set flds=?,mod=?,usn=? where id = ?", r) "update notes set flds=?,mod=?,usn=? where id = ?", r)
# Templates # Templates
@ -291,8 +291,8 @@ select id from cards where nid in (select id from notes where mid = ?)""",
return t return t
def addTemplate(self, m, template): def addTemplate(self, m, template):
"Note: should deck.genCards() afterwards." "Note: should col.genCards() afterwards."
self.deck.modSchema() self.col.modSchema()
m['tmpls'].append(template) m['tmpls'].append(template)
self._updateTemplOrds(m) self._updateTemplOrds(m)
self.save(m) self.save(m)
@ -301,12 +301,12 @@ select id from cards where nid in (select id from notes where mid = ?)""",
"False if removing template would leave orphan notes." "False if removing template would leave orphan notes."
# find cards using this template # find cards using this template
ord = m['tmpls'].index(template) ord = m['tmpls'].index(template)
cids = self.deck.db.list(""" cids = self.col.db.list("""
select c.id from cards c, notes f where c.nid=f.id and mid = ? and ord = ?""", select c.id from cards c, notes f where c.nid=f.id and mid = ? and ord = ?""",
m['id'], ord) m['id'], ord)
# all notes with this template must have at least two cards, or we # all notes with this template must have at least two cards, or we
# could end up creating orphaned notes # could end up creating orphaned notes
if self.deck.db.scalar(""" if self.col.db.scalar("""
select nid, count() from cards where select nid, count() from cards where
nid in (select nid from cards where id in %s) nid in (select nid from cards where id in %s)
group by nid group by nid
@ -314,13 +314,13 @@ having count() < 2
limit 1""" % ids2str(cids)): limit 1""" % ids2str(cids)):
return False return False
# ok to proceed; remove cards # ok to proceed; remove cards
self.deck.modSchema() self.col.modSchema()
self.deck.remCards(cids) self.col.remCards(cids)
# shift ordinals # shift ordinals
self.deck.db.execute(""" self.col.db.execute("""
update cards set ord = ord - 1, usn = ?, mod = ? update cards set ord = ord - 1, usn = ?, mod = ?
where nid in (select id from notes where mid = ?) and ord > ?""", where nid in (select id from notes where mid = ?) and ord > ?""",
self.deck.usn(), intTime(), m['id'], ord) self.col.usn(), intTime(), m['id'], ord)
m['tmpls'].remove(template) m['tmpls'].remove(template)
self._updateTemplOrds(m) self._updateTemplOrds(m)
self.save(m) self.save(m)
@ -344,10 +344,10 @@ update cards set ord = ord - 1, usn = ?, mod = ?
map.append("when ord = %d then %d" % (oldidxs[id(t)], t['ord'])) map.append("when ord = %d then %d" % (oldidxs[id(t)], t['ord']))
# apply # apply
self.save(m) self.save(m)
self.deck.db.execute(""" self.col.db.execute("""
update cards set ord = (case %s end),usn=?,mod=? where nid in ( update cards set ord = (case %s end),usn=?,mod=? where nid in (
select id from notes where mid = ?)""" % " ".join(map), select id from notes where mid = ?)""" % " ".join(map),
self.deck.usn(), intTime(), m['id']) self.col.usn(), intTime(), m['id'])
# Model changing # Model changing
########################################################################## ##########################################################################
@ -355,7 +355,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# - newModel should be self if model is not changing # - newModel should be self if model is not changing
def change(self, m, nids, newModel, fmap, cmap): def change(self, m, nids, newModel, fmap, cmap):
self.deck.modSchema() self.col.modSchema()
assert newModel['id'] == m['id'] or (fmap and cmap) assert newModel['id'] == m['id'] or (fmap and cmap)
if fmap: if fmap:
self._changeNotes(nids, newModel, fmap) self._changeNotes(nids, newModel, fmap)
@ -365,7 +365,7 @@ select id from notes where mid = ?)""" % " ".join(map),
def _changeNotes(self, nids, newModel, map): def _changeNotes(self, nids, newModel, map):
d = [] d = []
nfields = len(newModel['flds']) nfields = len(newModel['flds'])
for (nid, flds) in self.deck.db.execute( for (nid, flds) in self.col.db.execute(
"select id, flds from notes where id in "+ids2str(nids)): "select id, flds from notes where id in "+ids2str(nids)):
newflds = {} newflds = {}
flds = splitFields(flds) flds = splitFields(flds)
@ -376,25 +376,25 @@ select id from notes where mid = ?)""" % " ".join(map),
flds.append(newflds.get(c, "")) flds.append(newflds.get(c, ""))
flds = joinFields(flds) flds = joinFields(flds)
d.append(dict(nid=nid, flds=flds, mid=newModel['id'], d.append(dict(nid=nid, flds=flds, mid=newModel['id'],
m=intTime(),u=self.deck.usn())) m=intTime(),u=self.col.usn()))
self.deck.db.executemany( self.col.db.executemany(
"update notes set flds=:flds,mid=:mid,mod=:m,usn=:u where id = :nid", d) "update notes set flds=:flds,mid=:mid,mod=:m,usn=:u where id = :nid", d)
self.deck.updateFieldCache(nids) self.col.updateFieldCache(nids)
def _changeCards(self, nids, newModel, map): def _changeCards(self, nids, newModel, map):
d = [] d = []
deleted = [] deleted = []
for (cid, ord) in self.deck.db.execute( for (cid, ord) in self.col.db.execute(
"select id, ord from cards where nid in "+ids2str(nids)): "select id, ord from cards where nid in "+ids2str(nids)):
if map[ord] is not None: if map[ord] is not None:
d.append(dict( d.append(dict(
cid=cid,new=map[ord],u=self.deck.usn(),m=intTime())) cid=cid,new=map[ord],u=self.col.usn(),m=intTime()))
else: else:
deleted.append(cid) deleted.append(cid)
self.deck.db.executemany( self.col.db.executemany(
"update cards set ord=:new,usn=:u,mod=:m where id=:cid", "update cards set ord=:new,usn=:u,mod=:m where id=:cid",
d) d)
self.deck.remCards(deleted) self.col.remCards(deleted)
# Schema hash # Schema hash
########################################################################## ##########################################################################
@ -439,7 +439,7 @@ select id from notes where mid = ?)""" % " ".join(map),
a.append(cloze if cloze else "1") a.append(cloze if cloze else "1")
b.append("") b.append("")
data = [1, 1, m['id'], 1, t['ord'], "", joinFields(b)] data = [1, 1, m['id'], 1, t['ord'], "", joinFields(b)]
empty = self.deck._renderQA(data)['q'] empty = self.col._renderQA(data)['q']
start = a start = a
req = [] req = []
for i in range(len(flds)): for i in range(len(flds)):
@ -448,7 +448,7 @@ select id from notes where mid = ?)""" % " ".join(map),
# blank out this field # blank out this field
data[6] = joinFields(a) data[6] = joinFields(a)
# if the result is same as empty, field is required # if the result is same as empty, field is required
if self.deck._renderQA(data)['q'] == empty: if self.col._renderQA(data)['q'] == empty:
req.append(i) req.append(i)
return req, reqstrs return req, reqstrs

View file

@ -9,14 +9,14 @@ from anki.utils import fieldChecksum, intTime, \
class Note(object): class Note(object):
def __init__(self, deck, model=None, id=None): def __init__(self, col, model=None, id=None):
assert not (model and id) assert not (model and id)
self.deck = deck self.col = col
if id: if id:
self.id = id self.id = id
self.load() self.load()
else: else:
self.id = timestampID(deck.db, "notes") self.id = timestampID(col.db, "notes")
self.guid = guid64() self.guid = guid64()
self._model = model self._model = model
self.gid = model['gid'] self.gid = model['gid']
@ -25,7 +25,7 @@ class Note(object):
self.fields = [""] * len(self._model['flds']) self.fields = [""] * len(self._model['flds'])
self.flags = 0 self.flags = 0
self.data = "" self.data = ""
self._fmap = self.deck.models.fieldMap(self._model) self._fmap = self.col.models.fieldMap(self._model)
def load(self): def load(self):
(self.guid, (self.guid,
@ -36,29 +36,29 @@ class Note(object):
self.tags, self.tags,
self.fields, self.fields,
self.flags, self.flags,
self.data) = self.deck.db.first(""" self.data) = self.col.db.first("""
select guid, mid, gid, mod, usn, tags, flds, flags, data select guid, mid, gid, mod, usn, tags, flds, flags, data
from notes where id = ?""", self.id) from notes where id = ?""", self.id)
self.fields = splitFields(self.fields) self.fields = splitFields(self.fields)
self.tags = self.deck.tags.split(self.tags) self.tags = self.col.tags.split(self.tags)
self._model = self.deck.models.get(self.mid) self._model = self.col.models.get(self.mid)
self._fmap = self.deck.models.fieldMap(self._model) self._fmap = self.col.models.fieldMap(self._model)
def flush(self, mod=None): def flush(self, mod=None):
if self.model()['cloze']: if self.model()['cloze']:
self._clozePreFlush() self._clozePreFlush()
self.mod = mod if mod else intTime() self.mod = mod if mod else intTime()
self.usn = self.deck.usn() self.usn = self.col.usn()
sfld = stripHTML(self.fields[self.deck.models.sortIdx(self._model)]) sfld = stripHTML(self.fields[self.col.models.sortIdx(self._model)])
tags = self.stringTags() tags = self.stringTags()
res = self.deck.db.execute(""" res = self.col.db.execute("""
insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
self.id, self.guid, self.mid, self.gid, self.id, self.guid, self.mid, self.gid,
self.mod, self.usn, tags, self.mod, self.usn, tags,
self.joinedFields(), sfld, self.flags, self.data) self.joinedFields(), sfld, self.flags, self.data)
self.id = res.lastrowid self.id = res.lastrowid
self.updateFieldChecksums() self.updateFieldChecksums()
self.deck.tags.register(self.tags) self.col.tags.register(self.tags)
if self.model()['cloze']: if self.model()['cloze']:
self._clozePostFlush() self._clozePostFlush()
@ -66,7 +66,7 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
return joinFields(self.fields) return joinFields(self.fields)
def updateFieldChecksums(self): def updateFieldChecksums(self):
self.deck.db.execute("delete from nsums where nid = ?", self.id) self.col.db.execute("delete from nsums where nid = ?", self.id)
d = [] d = []
for (ord, conf) in self._fmap.values(): for (ord, conf) in self._fmap.values():
if not conf['uniq']: if not conf['uniq']:
@ -75,10 +75,10 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
if not val: if not val:
continue continue
d.append((self.id, self.mid, fieldChecksum(val))) d.append((self.id, self.mid, fieldChecksum(val)))
self.deck.db.executemany("insert into nsums values (?, ?, ?)", d) self.col.db.executemany("insert into nsums values (?, ?, ?)", d)
def cards(self): def cards(self):
return [self.deck.getCard(id) for id in self.deck.db.list( return [self.col.getCard(id) for id in self.col.db.list(
"select id from cards where nid = ? order by ord", self.id)] "select id from cards where nid = ? order by ord", self.id)]
def model(self): def model(self):
@ -119,13 +119,13 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
################################################## ##################################################
def hasTag(self, tag): def hasTag(self, tag):
return self.deck.tags.inList(tag, self.tags) return self.col.tags.inList(tag, self.tags)
def stringTags(self): def stringTags(self):
return self.deck.tags.join(self.deck.tags.canonify(self.tags)) return self.col.tags.join(self.col.tags.canonify(self.tags))
def setTagsFromStr(self, str): def setTagsFromStr(self, str):
self.tags = self.deck.tags.split(str) self.tags = self.col.tags.split(str)
def delTag(self, tag): def delTag(self, tag):
rem = [] rem = []
@ -154,14 +154,14 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
lim = "and nid != :nid" lim = "and nid != :nid"
else: else:
lim = "" lim = ""
nids = self.deck.db.list( nids = self.col.db.list(
"select nid from nsums where csum = ? and nid != ? and mid = ?", "select nid from nsums where csum = ? and nid != ? and mid = ?",
csum, self.id or 0, self.mid) csum, self.id or 0, self.mid)
if not nids: if not nids:
return True return True
# grab notes with the same checksums, and see if they're actually # grab notes with the same checksums, and see if they're actually
# duplicates # duplicates
for flds in self.deck.db.list("select flds from notes where id in "+ for flds in self.col.db.list("select flds from notes where id in "+
ids2str(nids)): ids2str(nids)):
fields = splitFields(flds) fields = splitFields(flds)
if fields[ord] == val: if fields[ord] == val:
@ -189,14 +189,14 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
################################################## ##################################################
def _clozePreFlush(self): def _clozePreFlush(self):
self.newlyAdded = not self.deck.db.scalar( self.newlyAdded = not self.col.db.scalar(
"select 1 from cards where nid = ?", self.id) "select 1 from cards where nid = ?", self.id)
tmpls = self.deck.findTemplates(self) tmpls = self.col.findTemplates(self)
ok = [] ok = []
for t in tmpls: for t in tmpls:
ok.append(t['ord']) ok.append(t['ord'])
# check if there are cards referencing a deleted cloze # check if there are cards referencing a deleted cloze
if self.deck.db.scalar( if self.col.db.scalar(
"select 1 from cards where nid = ? and ord not in %s" % "select 1 from cards where nid = ? and ord not in %s" %
ids2str(ok), self.id): ids2str(ok), self.id):
# there are; abort, as the UI should have handled this # there are; abort, as the UI should have handled this
@ -205,4 +205,4 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
def _clozePostFlush(self): def _clozePostFlush(self):
# generate missing cards # generate missing cards
if not self.newlyAdded: if not self.newlyAdded:
self.deck.genCards([self.id]) self.col.genCards([self.id])

View file

@ -18,8 +18,8 @@ from anki.hooks import runHook
# the standard Anki scheduler # the standard Anki scheduler
class Scheduler(object): class Scheduler(object):
name = "std" name = "std"
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
self.queueLimit = 50 self.queueLimit = 50
self.reportLimit = 1000 self.reportLimit = 1000
# fixme: replace reps with group based counts # fixme: replace reps with group based counts
@ -31,7 +31,7 @@ class Scheduler(object):
self._checkDay() self._checkDay()
id = self._getCardId() id = self._getCardId()
if id: if id:
c = self.deck.getCard(id) c = self.col.getCard(id)
c.startTimer() c.startTimer()
return c return c
@ -43,7 +43,7 @@ class Scheduler(object):
def answerCard(self, card, ease): def answerCard(self, card, ease):
assert ease >= 1 and ease <= 4 assert ease >= 1 and ease <= 4
self.deck.markReview(card) self.col.markReview(card)
self.reps += 1 self.reps += 1
card.reps += 1 card.reps += 1
wasNew = (card.queue == 0) and card.type != 2 wasNew = (card.queue == 0) and card.type != 2
@ -65,7 +65,7 @@ class Scheduler(object):
raise Exception("Invalid queue") raise Exception("Invalid queue")
self._updateStats(card, 'time', card.timeTaken()) self._updateStats(card, 'time', card.timeTaken())
card.mod = intTime() card.mod = intTime()
card.usn = self.deck.usn() card.usn = self.col.usn()
card.flushSched() card.flushSched()
def repCounts(self): def repCounts(self):
@ -76,7 +76,7 @@ class Scheduler(object):
def dueForecast(self, days=7): def dueForecast(self, days=7):
"Return counts over next DAYS. Includes today." "Return counts over next DAYS. Includes today."
daysd = dict(self.deck.db.all(""" daysd = dict(self.col.db.all("""
select due, count() from cards select due, count() from cards
where gid in %s and queue = 2 where gid in %s and queue = 2
and due between ? and ? and due between ? and ?
@ -103,7 +103,7 @@ order by due""" % self._groupLimit(),
def onClose(self): def onClose(self):
"Unbury and remove temporary suspends on close." "Unbury and remove temporary suspends on close."
self.deck.db.execute( self.col.db.execute(
"update cards set queue = type where queue between -3 and -2") "update cards set queue = type where queue between -3 and -2")
# Rev/lrn/time daily stats # Rev/lrn/time daily stats
@ -111,11 +111,11 @@ order by due""" % self._groupLimit(),
def _updateStats(self, card, type, cnt=1): def _updateStats(self, card, type, cnt=1):
key = type+"Today" key = type+"Today"
for g in ([self.deck.groups.get(card.gid)] + for g in ([self.col.groups.get(card.gid)] +
self.deck.groups.parents(card.gid)): self.col.groups.parents(card.gid)):
# add # add
g[key][1] += cnt g[key][1] += cnt
self.deck.groups.save(g) self.col.groups.save(g)
# Group counts # Group counts
########################################################################## ##########################################################################
@ -124,12 +124,12 @@ order by due""" % self._groupLimit(),
"Returns [groupname, gid, hasDue, hasNew]" "Returns [groupname, gid, hasDue, hasNew]"
# find groups with 1 or more due cards # find groups with 1 or more due cards
gids = {} gids = {}
for g in self.deck.groups.all(): for g in self.col.groups.all():
hasDue = self._groupHasLrn(g['id']) or self._groupHasRev(g['id']) hasDue = self._groupHasLrn(g['id']) or self._groupHasRev(g['id'])
hasNew = self._groupHasNew(g['id']) hasNew = self._groupHasNew(g['id'])
gids[g['id']] = [hasDue or 0, hasNew or 0] gids[g['id']] = [hasDue or 0, hasNew or 0]
return [[grp['name'], int(gid)]+gids[int(gid)] #.get(int(gid)) return [[grp['name'], int(gid)]+gids[int(gid)] #.get(int(gid))
for (gid, grp) in self.deck.groups.groups.items()] for (gid, grp) in self.col.groups.groups.items()]
def groupCountTree(self): def groupCountTree(self):
return self._groupChildren(self.groupCounts()) return self._groupChildren(self.groupCounts())
@ -138,7 +138,7 @@ order by due""" % self._groupLimit(),
"Like the count tree without the counts. Faster." "Like the count tree without the counts. Faster."
return self._groupChildren( return self._groupChildren(
[[grp['name'], int(gid), 0, 0, 0] [[grp['name'], int(gid), 0, 0, 0]
for (gid, grp) in self.deck.groups.groups.items()]) for (gid, grp) in self.col.groups.groups.items()])
def _groupChildren(self, grps): def _groupChildren(self, grps):
# first, split the group names into components # first, split the group names into components
@ -208,13 +208,13 @@ order by due""" % self._groupLimit(),
self.newCount = 0 self.newCount = 0
pcounts = {} pcounts = {}
# for each of the active groups # for each of the active groups
for gid in self.deck.groups.active(): for gid in self.col.groups.active():
# get the individual group's limit # get the individual group's limit
lim = self._groupNewLimitSingle(self.deck.groups.get(gid)) lim = self._groupNewLimitSingle(self.col.groups.get(gid))
if not lim: if not lim:
continue continue
# check the parents # check the parents
parents = self.deck.groups.parents(gid) parents = self.col.groups.parents(gid)
for p in parents: for p in parents:
# add if missing # add if missing
if p['id'] not in pcounts: if p['id'] not in pcounts:
@ -222,7 +222,7 @@ order by due""" % self._groupLimit(),
# take minimum of child and parent # take minimum of child and parent
lim = min(pcounts[p['id']], lim) lim = min(pcounts[p['id']], lim)
# see how many cards we actually have # see how many cards we actually have
cnt = self.deck.db.scalar(""" cnt = self.col.db.scalar("""
select count() from (select 1 from cards where select count() from (select 1 from cards where
gid = ? and queue = 0 limit ?)""", gid, lim) gid = ? and queue = 0 limit ?)""", gid, lim)
# if non-zero, decrement from parent counts # if non-zero, decrement from parent counts
@ -235,7 +235,7 @@ gid = ? and queue = 0 limit ?)""", gid, lim)
def _resetNew(self): def _resetNew(self):
self._resetNewCount() self._resetNewCount()
self.newGids = self.deck.groups.active() self.newGids = self.col.groups.active()
self._newQueue = [] self._newQueue = []
self._updateNewCardRatio() self._updateNewCardRatio()
@ -249,7 +249,7 @@ gid = ? and queue = 0 limit ?)""", gid, lim)
lim = min(self.queueLimit, self._groupNewLimit(gid)) lim = min(self.queueLimit, self._groupNewLimit(gid))
if lim: if lim:
# fill the queue with the current gid # fill the queue with the current gid
self._newQueue = self.deck.db.all(""" self._newQueue = self.col.db.all("""
select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim) select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
if self._newQueue: if self._newQueue:
self._newQueue.reverse() self._newQueue.reverse()
@ -262,7 +262,7 @@ select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
return return
(id, due) = self._newQueue.pop() (id, due) = self._newQueue.pop()
# move any siblings to the end? # move any siblings to the end?
conf = self.deck.groups.conf(self.newGids[0]) conf = self.col.groups.conf(self.newGids[0])
if conf['new']['order'] == NEW_TODAY_ORD: if conf['new']['order'] == NEW_TODAY_ORD:
n = len(self._newQueue) n = len(self._newQueue)
while self._newQueue and self._newQueue[-1][1] == due: while self._newQueue and self._newQueue[-1][1] == due:
@ -275,7 +275,7 @@ select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
return id return id
def _updateNewCardRatio(self): def _updateNewCardRatio(self):
if self.deck.groups.top()['newSpread'] == NEW_CARDS_DISTRIBUTE: if self.col.groups.top()['newSpread'] == NEW_CARDS_DISTRIBUTE:
if self.newCount: if self.newCount:
self.newCardModulus = ( self.newCardModulus = (
(self.newCount + self.revCount) / self.newCount) (self.newCount + self.revCount) / self.newCount)
@ -289,9 +289,9 @@ select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
"True if it's time to display a new card when distributing." "True if it's time to display a new card when distributing."
if not self.newCount: if not self.newCount:
return False return False
if self.deck.groups.top()['newSpread'] == NEW_CARDS_LAST: if self.col.groups.top()['newSpread'] == NEW_CARDS_LAST:
return False return False
elif self.deck.groups.top()['newSpread'] == NEW_CARDS_FIRST: elif self.col.groups.top()['newSpread'] == NEW_CARDS_FIRST:
return True return True
elif self.newCardModulus: elif self.newCardModulus:
return self.reps and self.reps % self.newCardModulus == 0 return self.reps and self.reps % self.newCardModulus == 0
@ -299,14 +299,14 @@ select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
def _groupHasNew(self, gid): def _groupHasNew(self, gid):
if not self._groupNewLimit(gid): if not self._groupNewLimit(gid):
return False return False
return self.deck.db.scalar( return self.col.db.scalar(
"select 1 from cards where gid = ? and queue = 0 limit 1", gid) "select 1 from cards where gid = ? and queue = 0 limit 1", gid)
def _groupNewLimit(self, gid): def _groupNewLimit(self, gid):
sel = self.deck.groups.get(gid) sel = self.col.groups.get(gid)
lim = -1 lim = -1
# for the group and each of its parents # for the group and each of its parents
for g in [sel] + self.deck.groups.parents(gid): for g in [sel] + self.col.groups.parents(gid):
rem = self._groupNewLimitSingle(g) rem = self._groupNewLimitSingle(g)
if lim == -1: if lim == -1:
lim = rem lim = rem
@ -315,14 +315,14 @@ select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
return lim return lim
def _groupNewLimitSingle(self, g): def _groupNewLimitSingle(self, g):
c = self.deck.groups.conf(g['id']) c = self.col.groups.conf(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1]) return max(0, c['new']['perDay'] - g['newToday'][1])
# Learning queue # Learning queue
########################################################################## ##########################################################################
def _resetLrnCount(self): def _resetLrnCount(self):
(self.lrnCount, self.lrnRepCount) = self.deck.db.first(""" (self.lrnCount, self.lrnRepCount) = self.col.db.first("""
select count(), sum(left) from (select left from cards where select count(), sum(left) from (select left from cards where
gid in %s and queue = 1 and due < ? limit %d)""" % ( gid in %s and queue = 1 and due < ? limit %d)""" % (
self._groupLimit(), self.reportLimit), self._groupLimit(), self.reportLimit),
@ -338,7 +338,7 @@ gid in %s and queue = 1 and due < ? limit %d)""" % (
return False return False
if self._lrnQueue: if self._lrnQueue:
return True return True
self._lrnQueue = self.deck.db.all(""" self._lrnQueue = self.col.db.all("""
select due, id from cards where select due, id from cards where
gid in %s and queue = 1 and due < :lim gid in %s and queue = 1 and due < :lim
limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff) limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
@ -350,7 +350,7 @@ limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
if self._fillLrn(): if self._fillLrn():
cutoff = time.time() cutoff = time.time()
if collapse: if collapse:
cutoff += self.deck.groups.top()['collapseTime'] cutoff += self.col.groups.top()['collapseTime']
if self._lrnQueue[0][0] < cutoff: if self._lrnQueue[0][0] < cutoff:
id = heappop(self._lrnQueue)[1] id = heappop(self._lrnQueue)[1]
self.lrnCount -= 1 self.lrnCount -= 1
@ -436,9 +436,9 @@ limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
lastIvl = -(self._delayForGrade(conf, lastLeft)) lastIvl = -(self._delayForGrade(conf, lastLeft))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
def log(): def log():
self.deck.db.execute( self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)", "insert into revlog values (?,?,?,?,?,?,?,?,?)",
int(time.time()*1000), card.id, self.deck.usn(), ease, int(time.time()*1000), card.id, self.col.usn(), ease,
ivl, lastIvl, card.factor, card.timeTaken(), type) ivl, lastIvl, card.factor, card.timeTaken(), type)
try: try:
log() log()
@ -452,33 +452,33 @@ limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
extra = "" extra = ""
if ids: if ids:
extra = " and id in "+ids2str(ids) extra = " and id in "+ids2str(ids)
self.deck.db.execute(""" self.col.db.execute("""
update cards set update cards set
due = edue, queue = 2, mod = %d, usn = %d due = edue, queue = 2, mod = %d, usn = %d
where queue = 1 and type = 2 where queue = 1 and type = 2
%s %s
""" % (intTime(), self.deck.usn(), extra)) """ % (intTime(), self.col.usn(), extra))
def _groupHasLrn(self, gid): def _groupHasLrn(self, gid):
return self.deck.db.scalar( return self.col.db.scalar(
"select 1 from cards where gid = ? and queue = 1 " "select 1 from cards where gid = ? and queue = 1 "
"and due < ? limit 1", "and due < ? limit 1",
gid, intTime() + self.deck.groups.top()['collapseTime']) gid, intTime() + self.col.groups.top()['collapseTime'])
# Reviews # Reviews
########################################################################## ##########################################################################
def _groupHasRev(self, gid): def _groupHasRev(self, gid):
return self.deck.db.scalar( return self.col.db.scalar(
"select 1 from cards where gid = ? and queue = 2 " "select 1 from cards where gid = ? and queue = 2 "
"and due <= ? limit 1", "and due <= ? limit 1",
gid, self.today) gid, self.today)
def _resetRevCount(self): def _resetRevCount(self):
top = self.deck.groups.top() top = self.col.groups.top()
lim = min(self.reportLimit, lim = min(self.reportLimit,
max(0, top['revLim'] - top['revToday'][1])) max(0, top['revLim'] - top['revToday'][1]))
self.revCount = self.deck.db.scalar(""" self.revCount = self.col.db.scalar("""
select count() from (select id from cards where select count() from (select id from cards where
gid in %s and queue = 2 and due <= :day limit %d)""" % ( gid in %s and queue = 2 and due <= :day limit %d)""" % (
self._groupLimit(), lim), day=self.today) self._groupLimit(), lim), day=self.today)
@ -492,12 +492,12 @@ gid in %s and queue = 2 and due <= :day limit %d)""" % (
return False return False
if self._revQueue: if self._revQueue:
return True return True
self._revQueue = self.deck.db.list(""" self._revQueue = self.col.db.list("""
select id from cards where select id from cards where
gid in %s and queue = 2 and due <= :lim %s limit %d""" % ( gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
self._groupLimit(), self._revOrder(), self.queueLimit), self._groupLimit(), self._revOrder(), self.queueLimit),
lim=self.today) lim=self.today)
if not self.deck.conf['revOrder']: if not self.col.conf['revOrder']:
r = random.Random() r = random.Random()
r.seed(self.today) r.seed(self.today)
r.shuffle(self._revQueue) r.shuffle(self._revQueue)
@ -509,8 +509,8 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
return self._revQueue.pop() return self._revQueue.pop()
def _revOrder(self): def _revOrder(self):
if self.deck.conf['revOrder']: if self.col.conf['revOrder']:
return "order by %s" % ("ivl desc", "ivl")[self.deck.conf['revOrder']-1] return "order by %s" % ("ivl desc", "ivl")[self.col.conf['revOrder']-1]
return "" return ""
# Answering a review card # Answering a review card
@ -555,9 +555,9 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
def _logRev(self, card, ease): def _logRev(self, card, ease):
def log(): def log():
self.deck.db.execute( self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)", "insert into revlog values (?,?,?,?,?,?,?,?,?)",
int(time.time()*1000), card.id, self.deck.usn(), ease, int(time.time()*1000), card.id, self.col.usn(), ease,
card.ivl, card.lastIvl, card.factor, card.timeTaken(), card.ivl, card.lastIvl, card.factor, card.timeTaken(),
1) 1)
try: try:
@ -604,7 +604,7 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
idealDue = self.today + idealIvl idealDue = self.today + idealIvl
conf = self._cardConf(card)['rev'] conf = self._cardConf(card)['rev']
# find sibling positions # find sibling positions
dues = self.deck.db.list( dues = self.col.db.list(
"select due from cards where nid = ? and queue = 2" "select due from cards where nid = ? and queue = 2"
" and id != ?", card.nid, card.id) " and id != ?", card.nid, card.id)
if not dues or idealDue not in dues: if not dues or idealDue not in dues:
@ -653,19 +653,19 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
########################################################################## ##########################################################################
def _cardConf(self, card): def _cardConf(self, card):
return self.deck.groups.conf(card.gid) return self.col.groups.conf(card.gid)
def _groupLimit(self): def _groupLimit(self):
return ids2str(self.deck.groups.active()) return ids2str(self.col.groups.active())
# Daily cutoff # Daily cutoff
########################################################################## ##########################################################################
def _updateCutoff(self): def _updateCutoff(self):
# days since deck created # days since col created
self.today = int((time.time() - self.deck.crt) / 86400) self.today = int((time.time() - self.col.crt) / 86400)
# end of day cutoff # end of day cutoff
self.dayCutoff = self.deck.crt + (self.today+1)*86400 self.dayCutoff = self.col.crt + (self.today+1)*86400
# update all selected groups # update all selected groups
def update(g): def update(g):
save = False save = False
@ -675,11 +675,11 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
save = True save = True
g[key] = [self.today, 0] g[key] = [self.today, 0]
if save: if save:
self.deck.groups.save(g) self.col.groups.save(g)
for gid in self.deck.groups.active(): for gid in self.col.groups.active():
update(self.deck.groups.get(gid)) update(self.col.groups.get(gid))
# update parents too # update parents too
for grp in self.deck.groups.parents(self.deck.groups.selected()): for grp in self.col.groups.parents(self.col.groups.selected()):
update(grp) update(grp)
def _checkDay(self): def _checkDay(self):
@ -712,14 +712,14 @@ your short-term review workload will become."""))
def revDue(self): def revDue(self):
"True if there are any rev cards due." "True if there are any rev cards due."
return self.deck.db.scalar( return self.col.db.scalar(
("select 1 from cards where gid in %s and queue = 2 " ("select 1 from cards where gid in %s and queue = 2 "
"and due <= ? limit 1") % self._groupLimit(), "and due <= ? limit 1") % self._groupLimit(),
self.today) self.today)
def newDue(self): def newDue(self):
"True if there are any new cards due." "True if there are any new cards due."
return self.deck.db.scalar( return self.col.db.scalar(
("select 1 from cards where gid in %s and queue = 0 " ("select 1 from cards where gid in %s and queue = 0 "
"limit 1") % self._groupLimit()) "limit 1") % self._groupLimit())
@ -771,34 +771,34 @@ your short-term review workload will become."""))
def suspendCards(self, ids): def suspendCards(self, ids):
"Suspend cards." "Suspend cards."
self.removeFailed(ids) self.removeFailed(ids)
self.deck.db.execute( self.col.db.execute(
"update cards set queue=-1,mod=?,usn=? where id in "+ "update cards set queue=-1,mod=?,usn=? where id in "+
ids2str(ids), intTime(), self.deck.usn()) ids2str(ids), intTime(), self.col.usn())
def unsuspendCards(self, ids): def unsuspendCards(self, ids):
"Unsuspend cards." "Unsuspend cards."
self.deck.db.execute( self.col.db.execute(
"update cards set queue=type,mod=?,usn=? " "update cards set queue=type,mod=?,usn=? "
"where queue = -1 and id in "+ ids2str(ids), "where queue = -1 and id in "+ ids2str(ids),
intTime(), self.deck.usn()) intTime(), self.col.usn())
def buryNote(self, nid): def buryNote(self, nid):
"Bury all cards for note until next session." "Bury all cards for note until next session."
self.deck.setDirty() self.col.setDirty()
self.removeFailed( self.removeFailed(
self.deck.db.list("select id from cards where nid = ?", nid)) self.col.db.list("select id from cards where nid = ?", nid))
self.deck.db.execute("update cards set queue = -2 where nid = ?", nid) self.col.db.execute("update cards set queue = -2 where nid = ?", nid)
# Resetting # Resetting
########################################################################## ##########################################################################
def forgetCards(self, ids): def forgetCards(self, ids):
"Put cards at the end of the new queue." "Put cards at the end of the new queue."
self.deck.db.execute( self.col.db.execute(
"update cards set type=0,queue=0,ivl=0 where id in "+ids2str(ids)) "update cards set type=0,queue=0,ivl=0 where id in "+ids2str(ids))
pmax = self.deck.db.scalar("select max(due) from cards where type=0") pmax = self.col.db.scalar("select max(due) from cards where type=0")
# takes care of mod + usn # takes care of mod + usn
self.sortCards(ids, start=pmax+1, shuffle=self.deck.models.randomNew()) self.sortCards(ids, start=pmax+1, shuffle=self.col.models.randomNew())
def reschedCards(self, ids, imin, imax): def reschedCards(self, ids, imin, imax):
"Put cards in review queue with a new interval in days (min, max)." "Put cards in review queue with a new interval in days (min, max)."
@ -808,7 +808,7 @@ your short-term review workload will become."""))
for id in ids: for id in ids:
r = random.randint(imin, imax) r = random.randint(imin, imax)
d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod)) d.append(dict(id=id, due=r+t, ivl=max(1, r), mod=mod))
self.deck.db.executemany( self.col.db.executemany(
"update cards set type=2,queue=2,ivl=:ivl,due=:due where id=:id", "update cards set type=2,queue=2,ivl=:ivl,due=:due where id=:id",
d) d)
@ -818,7 +818,7 @@ your short-term review workload will become."""))
def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False):
scids = ids2str(cids) scids = ids2str(cids)
now = intTime() now = intTime()
nids = self.deck.db.list( nids = self.col.db.list(
("select distinct nid from cards where type = 0 and id in %s " ("select distinct nid from cards where type = 0 and id in %s "
"order by nid") % scids) "order by nid") % scids)
if not nids: if not nids:
@ -833,27 +833,27 @@ your short-term review workload will become."""))
high = start+c*step high = start+c*step
# shift? # shift?
if shift: if shift:
low = self.deck.db.scalar( low = self.col.db.scalar(
"select min(due) from cards where due >= ? and type = 0 " "select min(due) from cards where due >= ? and type = 0 "
"and id not in %s" % scids, "and id not in %s" % scids,
start) start)
if low is not None: if low is not None:
shiftby = high - low + 1 shiftby = high - low + 1
self.deck.db.execute(""" self.col.db.execute("""
update cards set mod=?, usn=?, due=due+? where id not in %s update cards set mod=?, usn=?, due=due+? where id not in %s
and due >= ?""" % scids, now, self.deck.usn(), shiftby, low) and due >= ?""" % scids, now, self.col.usn(), shiftby, low)
# reorder cards # reorder cards
d = [] d = []
for id, nid in self.deck.db.execute( for id, nid in self.col.db.execute(
"select id, nid from cards where type = 0 and id in "+scids): "select id, nid from cards where type = 0 and id in "+scids):
d.append(dict(now=now, due=due[nid], usn=self.deck.usn(), cid=id)) d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id))
self.deck.db.executemany( self.col.db.executemany(
"update cards set due=:due,mod=:now,usn=:usn where id = :cid""", d) "update cards set due=:due,mod=:now,usn=:usn where id = :cid""", d)
# fixme: because it's a model property now, these should be done on a # fixme: because it's a model property now, these should be done on a
# per-model basis # per-model basis
def randomizeCards(self): def randomizeCards(self):
self.sortCards(self.deck.db.list("select id from cards"), shuffle=True) self.sortCards(self.col.db.list("select id from cards"), shuffle=True)
def orderCards(self): def orderCards(self):
self.sortCards(self.deck.db.list("select id from cards")) self.sortCards(self.col.db.list("select id from cards"))

View file

@ -192,7 +192,7 @@ def stopMplayer(*args):
return return
mplayerManager.kill() mplayerManager.kill()
addHook("deckClosed", stopMplayer) addHook("colClosed", stopMplayer)
# PyAudio recording # PyAudio recording
########################################################################## ##########################################################################

View file

@ -14,8 +14,8 @@ from anki.hooks import runFilter
class CardStats(object): class CardStats(object):
def __init__(self, deck, card): def __init__(self, col, card):
self.deck = deck self.col = col
self.card = card self.card = card
def report(self): def report(self):
@ -23,23 +23,23 @@ class CardStats(object):
fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs) fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs)
self.txt = "<table width=100%%>" self.txt = "<table width=100%%>"
self.addLine(_("Added"), self.date(c.id/1000)) self.addLine(_("Added"), self.date(c.id/1000))
first = self.deck.db.scalar( first = self.col.db.scalar(
"select min(id) from revlog where cid = ?", c.id) "select min(id) from revlog where cid = ?", c.id)
last = self.deck.db.scalar( last = self.col.db.scalar(
"select max(id) from revlog where cid = ?", c.id) "select max(id) from revlog where cid = ?", c.id)
if first: if first:
self.addLine(_("First Review"), self.date(first/1000)) self.addLine(_("First Review"), self.date(first/1000))
self.addLine(_("Latest Review"), self.date(last/1000)) self.addLine(_("Latest Review"), self.date(last/1000))
if c.queue in (1,2): if c.queue in (1,2):
if c.queue == 2: if c.queue == 2:
next = time.time()+((self.deck.sched.today - c.due)*86400) next = time.time()+((self.col.sched.today - c.due)*86400)
else: else:
next = c.due next = c.due
next = self.date(next) next = self.date(next)
self.addLine(_("Due"), next) self.addLine(_("Due"), next)
self.addLine(_("Interval"), fmt(c.ivl * 86400)) self.addLine(_("Interval"), fmt(c.ivl * 86400))
self.addLine(_("Ease"), "%d%%" % (c.factor/10.0)) self.addLine(_("Ease"), "%d%%" % (c.factor/10.0))
(cnt, total) = self.deck.db.first( (cnt, total) = self.col.db.first(
"select count(), sum(time)/1000 from revlog where cid = :id", "select count(), sum(time)/1000 from revlog where cid = :id",
id=c.id) id=c.id)
if cnt: if cnt:
@ -49,8 +49,8 @@ class CardStats(object):
self.addLine(_("Position"), c.due) self.addLine(_("Position"), c.due)
self.addLine(_("Model"), c.model()['name']) self.addLine(_("Model"), c.model()['name'])
self.addLine(_("Template"), c.template()['name']) self.addLine(_("Template"), c.template()['name'])
self.addLine(_("Current Group"), self.deck.groups.name(c.gid)) self.addLine(_("Current Group"), self.col.groups.name(c.gid))
self.addLine(_("Home Group"), self.deck.groups.name(c.note().gid)) self.addLine(_("Home Group"), self.col.groups.name(c.note().gid))
self.txt += "</table>" self.txt += "</table>"
return self.txt return self.txt
@ -73,7 +73,7 @@ class CardStats(object):
str += fmtTimeSpan(tm%60, point=2 if not str else -1, short=True) str += fmtTimeSpan(tm%60, point=2 if not str else -1, short=True)
return str return str
# Deck stats # Collection stats
########################################################################## ##########################################################################
colYoung = "#7c7" colYoung = "#7c7"
@ -88,10 +88,10 @@ colTime = "#770"
colUnseen = "#000" colUnseen = "#000"
colSusp = "#ff0" colSusp = "#ff0"
class DeckStats(object): class CollectionStats(object):
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
self._stats = None self._stats = None
self.type = 0 self.type = 0
self.width = 600 self.width = 600
@ -173,7 +173,7 @@ table * { font-size: 14px; }
lim += " and due-:today >= %d" % start lim += " and due-:today >= %d" % start
if end is not None: if end is not None:
lim += " and day < %d" % end lim += " and day < %d" % end
return self.deck.db.all(""" return self.col.db.all("""
select (due-:today)/:chunk as day, select (due-:today)/:chunk as day,
sum(case when ivl < 21 then 1 else 0 end), -- yng sum(case when ivl < 21 then 1 else 0 end), -- yng
sum(case when ivl >= 21 then 1 else 0 end) -- mtr sum(case when ivl >= 21 then 1 else 0 end) -- mtr
@ -181,7 +181,7 @@ from cards
where gid in %s and queue = 2 where gid in %s and queue = 2
%s %s
group by day order by day""" % (self._limit(), lim), group by day order by day""" % (self._limit(), lim),
today=self.deck.sched.today, today=self.col.sched.today,
chunk=chunk) chunk=chunk)
# Reps and time spent # Reps and time spent
@ -248,7 +248,7 @@ group by day order by day""" % (self._limit(), lim),
tot = totd[-1][1] tot = totd[-1][1]
period = self._periodDays() period = self._periodDays()
if not period: if not period:
period = self.deck.sched.today - first + 1 period = self.col.sched.today - first + 1
i = [] i = []
self._line(i, _("Days studied"), self._line(i, _("Days studied"),
_("<b>%(pct)d%%</b> (%(x)s of %(y)s)") % dict( _("<b>%(pct)d%%</b> (%(x)s of %(y)s)") % dict(
@ -303,7 +303,7 @@ group by day order by day""" % (self._limit(), lim),
lims = [] lims = []
if num is not None: if num is not None:
lims.append("id > %d" % ( lims.append("id > %d" % (
(self.deck.sched.dayCutoff-(num*chunk*86400))*1000)) (self.col.sched.dayCutoff-(num*chunk*86400))*1000))
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: if lim:
lims.append(lim) lims.append(lim)
@ -315,7 +315,7 @@ group by day order by day""" % (self._limit(), lim),
tf = 60.0 # minutes tf = 60.0 # minutes
else: else:
tf = 3600.0 # hours tf = 3600.0 # hours
return self.deck.db.all(""" return self.col.db.all("""
select select
(cast((id/1000 - :cut) / 86400.0 as int))/:chunk as day, (cast((id/1000 - :cut) / 86400.0 as int))/:chunk as day,
sum(case when type = 0 then 1 else 0 end), -- lrn count sum(case when type = 0 then 1 else 0 end), -- lrn count
@ -331,7 +331,7 @@ sum(case when type = 2 then time/1000 else 0 end)/:tf, -- lapse time
sum(case when type = 3 then time/1000 else 0 end)/:tf -- cram time sum(case when type = 3 then time/1000 else 0 end)/:tf -- cram time
from revlog %s from revlog %s
group by day order by day""" % lim, group by day order by day""" % lim,
cut=self.deck.sched.dayCutoff, cut=self.col.sched.dayCutoff,
tf=tf, tf=tf,
chunk=chunk) chunk=chunk)
@ -341,7 +341,7 @@ group by day order by day""" % lim,
if num: if num:
lims.append( lims.append(
"id > %d" % "id > %d" %
((self.deck.sched.dayCutoff-(num*86400))*1000)) ((self.col.sched.dayCutoff-(num*86400))*1000))
rlim = self._revlogLimit() rlim = self._revlogLimit()
if rlim: if rlim:
lims.append(rlim) lims.append(rlim)
@ -349,12 +349,12 @@ group by day order by day""" % lim,
lim = "where " + " and ".join(lims) lim = "where " + " and ".join(lims)
else: else:
lim = "" lim = ""
return self.deck.db.first(""" return self.col.db.first("""
select count(), abs(min(day)) from (select select count(), abs(min(day)) from (select
(cast((id/1000 - :cut) / 86400.0 as int)+1) as day (cast((id/1000 - :cut) / 86400.0 as int)+1) as day
from revlog %s from revlog %s
group by day order by day)""" % lim, group by day order by day)""" % lim,
cut=self.deck.sched.dayCutoff) cut=self.col.sched.dayCutoff)
# Intervals # Intervals
###################################################################### ######################################################################
@ -389,12 +389,12 @@ group by day order by day)""" % lim,
chunk = 7; lim = " and grp <= 52" chunk = 7; lim = " and grp <= 52"
else: else:
chunk = 30; lim = "" chunk = 30; lim = ""
data = [self.deck.db.all(""" data = [self.col.db.all("""
select ivl / :chunk as grp, count() from cards select ivl / :chunk as grp, count() from cards
where gid in %s and queue = 2 %s where gid in %s and queue = 2 %s
group by grp group by grp
order by grp""" % (self._limit(), lim), chunk=chunk)] order by grp""" % (self._limit(), lim), chunk=chunk)]
return data + list(self.deck.db.first(""" return data + list(self.col.db.first("""
select count(), avg(ivl), max(ivl) from cards where gid in %s and queue = 2""" % select count(), avg(ivl), max(ivl) from cards where gid in %s and queue = 2""" %
self._limit())) self._limit()))
@ -457,7 +457,7 @@ select count(), avg(ivl), max(ivl) from cards where gid in %s and queue = 2""" %
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: if lim:
lim = "where " + lim lim = "where " + lim
return self.deck.db.all(""" return self.col.db.all("""
select (case select (case
when type in (0,2) then 0 when type in (0,2) then 0
when lastIvl < 21 then 1 when lastIvl < 21 then 1
@ -511,8 +511,8 @@ order by thetype, ease""" % lim)
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: if lim:
lim = " and " + lim lim = " and " + lim
sd = datetime.datetime.fromtimestamp(self.deck.crt) sd = datetime.datetime.fromtimestamp(self.col.crt)
return self.deck.db.all(""" return self.col.db.all("""
select select
23 - ((cast((:cut - id/1000) / 3600.0 as int)) %% 24) as hour, 23 - ((cast((:cut - id/1000) / 3600.0 as int)) %% 24) as hour,
sum(case when ease = 1 then 0 else 1 end) / sum(case when ease = 1 then 0 else 1 end) /
@ -520,7 +520,7 @@ cast(count() as float) * 100,
count() count()
from revlog where type = 1 %s from revlog where type = 1 %s
group by hour having count() > 30 order by hour""" % lim, group by hour having count() > 30 order by hour""" % lim,
cut=self.deck.sched.dayCutoff-(sd.hour*3600)) cut=self.col.sched.dayCutoff-(sd.hour*3600))
# Cards # Cards
###################################################################### ######################################################################
@ -537,7 +537,7 @@ group by hour having count() > 30 order by hour""" % lim,
d.append(dict(data=div[c], label=t, color=col)) d.append(dict(data=div[c], label=t, color=col))
# text data # text data
i = [] i = []
(c, f) = self.deck.db.first(""" (c, f) = self.col.db.first("""
select count(id), count(distinct nid) from cards select count(id), count(distinct nid) from cards
where gid in %s """ % self._limit()) where gid in %s """ % self._limit())
self._line(i, _("Total cards"), c) self._line(i, _("Total cards"), c)
@ -547,7 +547,7 @@ where gid in %s """ % self._limit())
self._line(i, _("Lowest ease factor"), "%d%%" % low) self._line(i, _("Lowest ease factor"), "%d%%" % low)
self._line(i, _("Average ease factor"), "%d%%" % avg) self._line(i, _("Average ease factor"), "%d%%" % avg)
self._line(i, _("Highest ease factor"), "%d%%" % high) self._line(i, _("Highest ease factor"), "%d%%" % high)
min = self.deck.db.scalar( min = self.col.db.scalar(
"select min(id) from cards where gid in %s " % self._limit()) "select min(id) from cards where gid in %s " % self._limit())
if min: if min:
self._line(i, _("First card created"), _("%s ago") % fmtTimeSpan( self._line(i, _("First card created"), _("%s ago") % fmtTimeSpan(
@ -557,7 +557,7 @@ where gid in %s """ % self._limit())
A card's <i>ease factor</i> is the size of the next interval \ A card's <i>ease factor</i> is the size of the next interval \
when you answer "good" on a review.''') when you answer "good" on a review.''')
txt = self._title(_("Cards Types"), txt = self._title(_("Cards Types"),
_("The division of cards in your deck.")) _("The division of cards in your deck(s)."))
txt += "<table width=%d><tr><td>%s</td><td>%s</td></table>" % ( txt += "<table width=%d><tr><td>%s</td><td>%s</td></table>" % (
self.width, self.width,
self._graph(id="cards", data=d, type="pie"), self._graph(id="cards", data=d, type="pie"),
@ -574,7 +574,7 @@ when you answer "good" on a review.''')
return "<table width=400>" + "".join(i) + "</table>" return "<table width=400>" + "".join(i) + "</table>"
def _factors(self): def _factors(self):
return self.deck.db.first(""" return self.col.db.first("""
select select
min(factor) / 10.0, min(factor) / 10.0,
avg(factor) / 10.0, avg(factor) / 10.0,
@ -582,7 +582,7 @@ max(factor) / 10.0
from cards where gid in %s and queue = 2""" % self._limit()) from cards where gid in %s and queue = 2""" % self._limit())
def _cards(self): def _cards(self):
return self.deck.db.first(""" return self.col.db.first("""
select select
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
sum(case when queue=1 or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn sum(case when queue=1 or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
@ -668,11 +668,11 @@ $(function () {
data=simplejson.dumps(data), conf=simplejson.dumps(conf))) data=simplejson.dumps(data), conf=simplejson.dumps(conf)))
def _limit(self): def _limit(self):
return self.deck.sched._groupLimit() return self.col.sched._groupLimit()
def _revlogLimit(self): def _revlogLimit(self):
return ("cid in (select id from cards where gid in %s)" % return ("cid in (select id from cards where gid in %s)" %
ids2str(self.deck.groups.active())) ids2str(self.col.groups.active()))
def _title(self, title, subtitle=""): def _title(self, title, subtitle=""):
return '<h1>%s</h1>%s' % (title, subtitle) return '<h1>%s</h1>%s' % (title, subtitle)

View file

@ -9,8 +9,8 @@ models = []
# Basic # Basic
########################################################################## ##########################################################################
def addBasicModel(deck): def addBasicModel(col):
mm = deck.models mm = col.models
m = mm.new(_("Basic")) m = mm.new(_("Basic"))
fm = mm.newField(_("Front")) fm = mm.newField(_("Front"))
fm['req'] = True fm['req'] = True
@ -30,8 +30,8 @@ models.append((_("Basic"), addBasicModel))
# Cloze # Cloze
########################################################################## ##########################################################################
def addClozeModel(deck): def addClozeModel(col):
mm = deck.models mm = col.models
m = mm.new(_("Cloze")) m = mm.new(_("Cloze"))
fm = mm.newField(_("Text")) fm = mm.newField(_("Text"))
fm['req'] = True fm['req'] = True

View file

@ -6,12 +6,12 @@ import os, simplejson
from anki.lang import _ from anki.lang import _
from anki.utils import intTime from anki.utils import intTime
from anki.db import DB from anki.db import DB
from anki.deck import _Deck from anki.collection import _Collection
from anki.consts import * from anki.consts import *
from anki.stdmodels import addBasicModel, addClozeModel from anki.stdmodels import addBasicModel, addClozeModel
def Deck(path, queue=True, lock=True, server=False): def Collection(path, queue=True, lock=True, server=False):
"Open a new or existing deck. Path must be unicode." "Open a new or existing collection. Path must be unicode."
assert path.endswith(".anki2") assert path.endswith(".anki2")
path = os.path.abspath(path) path = os.path.abspath(path)
create = not os.path.exists(path) create = not os.path.exists(path)
@ -27,30 +27,30 @@ def Deck(path, queue=True, lock=True, server=False):
ver = _upgradeSchema(db) ver = _upgradeSchema(db)
db.execute("pragma temp_store = memory") db.execute("pragma temp_store = memory")
db.execute("pragma cache_size = 10000") db.execute("pragma cache_size = 10000")
# add db to deck and do any remaining upgrades # add db to col and do any remaining upgrades
deck = _Deck(db, server) col = _Collection(db, server)
if ver < SCHEMA_VERSION: if ver < SCHEMA_VERSION:
_upgradeDeck(deck, ver) _upgrade(col, ver)
elif create: elif create:
# add in reverse order so basic is default # add in reverse order so basic is default
addClozeModel(deck) addClozeModel(col)
addBasicModel(deck) addBasicModel(col)
deck.save() col.save()
if lock: if lock:
deck.lock() col.lock()
if not queue: if not queue:
return deck return col
# rebuild queue # rebuild queue
deck.reset() col.reset()
return deck return col
# no upgrades necessary at the moment # no upgrades necessary at the moment
def _upgradeSchema(db): def _upgradeSchema(db):
return SCHEMA_VERSION return SCHEMA_VERSION
def _upgradeDeck(deck, ver): def _upgrade(col, ver):
return return
# Creating a new deck # Creating a new collection
###################################################################### ######################################################################
def _createDB(db): def _createDB(db):
@ -62,9 +62,9 @@ def _createDB(db):
db.execute("analyze") db.execute("analyze")
return SCHEMA_VERSION return SCHEMA_VERSION
def _addSchema(db, setDeckConf=True): def _addSchema(db, setColConf=True):
db.executescript(""" db.executescript("""
create table if not exists deck ( create table if not exists col (
id integer primary key, id integer primary key,
crt integer not null, crt integer not null,
mod integer not null, mod integer not null,
@ -137,14 +137,14 @@ create table if not exists graves (
type integer not null type integer not null
); );
insert or ignore into deck insert or ignore into col
values(1,0,0,0,%(v)s,0,0,0,'','{}','','','{}'); values(1,0,0,0,%(v)s,0,0,0,'','{}','','','{}');
""" % ({'v':SCHEMA_VERSION})) """ % ({'v':SCHEMA_VERSION}))
import anki.deck if setColConf:
if setDeckConf: _addColVars(db, *_getColVars(db))
_addDeckVars(db, *_getDeckVars(db))
def _getDeckVars(db): def _getColVars(db):
import anki.collection
import anki.groups import anki.groups
g = anki.groups.defaultGroup.copy() g = anki.groups.defaultGroup.copy()
for k,v in anki.groups.defaultTopConf.items(): for k,v in anki.groups.defaultTopConf.items():
@ -155,11 +155,11 @@ def _getDeckVars(db):
g['mod'] = intTime() g['mod'] = intTime()
gc = anki.groups.defaultConf.copy() gc = anki.groups.defaultConf.copy()
gc['id'] = 1 gc['id'] = 1
return g, gc, anki.deck.defaultConf.copy() return g, gc, anki.collection.defaultConf.copy()
def _addDeckVars(db, g, gc, c): def _addColVars(db, g, gc, c):
db.execute(""" db.execute("""
update deck set conf = ?, groups = ?, gconf = ?""", update col set conf = ?, groups = ?, gconf = ?""",
simplejson.dumps(c), simplejson.dumps(c),
simplejson.dumps({'1': g}), simplejson.dumps({'1': g}),
simplejson.dumps({'1': gc})) simplejson.dumps({'1': gc}))

View file

@ -23,8 +23,8 @@ if simplejson.__version__ < "1.7.3":
# todo: # todo:
# - ensure all urllib references are converted to urllib2 for proxies # - ensure all urllib references are converted to urllib2 for proxies
# - ability to cancel # - ability to cancel
# - need to make sure syncing doesn't bump the deck modified time if nothing was # - need to make sure syncing doesn't bump the col modified time if nothing was
# changed, since by default closing the deck bumps the mod time # changed, since by default closing the col bumps the mod time
# - ensure the user doesn't add foreign chars to passsword # - ensure the user doesn't add foreign chars to passsword
# Incremental syncing # Incremental syncing
@ -34,8 +34,8 @@ from anki.consts import *
class Syncer(object): class Syncer(object):
def __init__(self, deck, server=None): def __init__(self, col, server=None):
self.deck = deck self.col = col
self.server = server self.server = server
def status(self, type): def status(self, type):
@ -90,7 +90,7 @@ class Syncer(object):
return "success" return "success"
def meta(self): def meta(self):
return (self.deck.mod, self.deck.scm, self.deck._usn, intTime(), None) return (self.col.mod, self.col.scm, self.col._usn, intTime(), None)
def changes(self): def changes(self):
"Bundle up deletions and small objects, and apply if server." "Bundle up deletions and small objects, and apply if server."
@ -104,7 +104,7 @@ class Syncer(object):
def applyChanges(self, minUsn, lnewer, changes): def applyChanges(self, minUsn, lnewer, changes):
# we're the server; save info # we're the server; save info
self.maxUsn = self.deck._usn self.maxUsn = self.col._usn
self.minUsn = minUsn self.minUsn = minUsn
self.lnewer = not lnewer self.lnewer = not lnewer
self.rchg = changes self.rchg = changes
@ -127,33 +127,33 @@ class Syncer(object):
def sanityCheck(self): def sanityCheck(self):
# some basic checks to ensure the sync went ok. this is slow, so will # some basic checks to ensure the sync went ok. this is slow, so will
# be removed before official release # be removed before official release
assert not self.deck.db.scalar(""" assert not self.col.db.scalar("""
select count() from cards where nid not in (select id from notes)""") select count() from cards where nid not in (select id from notes)""")
assert not self.deck.db.scalar(""" assert not self.col.db.scalar("""
select count() from notes where id not in (select distinct nid from cards)""") select count() from notes where id not in (select distinct nid from cards)""")
for t in "cards", "notes", "revlog", "graves": for t in "cards", "notes", "revlog", "graves":
assert not self.deck.db.scalar( assert not self.col.db.scalar(
"select count() from %s where usn = -1" % t) "select count() from %s where usn = -1" % t)
for g in self.deck.groups.all(): for g in self.col.groups.all():
assert g['usn'] != -1 assert g['usn'] != -1
for t, usn in self.deck.tags.allItems(): for t, usn in self.col.tags.allItems():
assert usn != -1 assert usn != -1
for m in self.deck.models.all(): for m in self.col.models.all():
assert m['usn'] != -1 assert m['usn'] != -1
return [ return [
self.deck.db.scalar("select count() from cards"), self.col.db.scalar("select count() from cards"),
self.deck.db.scalar("select count() from notes"), self.col.db.scalar("select count() from notes"),
self.deck.db.scalar("select count() from revlog"), self.col.db.scalar("select count() from revlog"),
self.deck.db.scalar("select count() from nsums"), self.col.db.scalar("select count() from nsums"),
self.deck.db.scalar("select count() from graves"), self.col.db.scalar("select count() from graves"),
len(self.deck.models.all()), len(self.col.models.all()),
len(self.deck.tags.all()), len(self.col.tags.all()),
len(self.deck.groups.all()), len(self.col.groups.all()),
len(self.deck.groups.allConf()), len(self.col.groups.allConf()),
] ]
def usnLim(self): def usnLim(self):
if self.deck.server: if self.col.server:
return "usn >= %d" % self.minUsn return "usn >= %d" % self.minUsn
else: else:
return "usn = -1" return "usn = -1"
@ -162,9 +162,9 @@ select count() from notes where id not in (select distinct nid from cards)""")
if not mod: if not mod:
# server side; we decide new mod time # server side; we decide new mod time
mod = intTime(1000) mod = intTime(1000)
self.deck.ls = mod self.col.ls = mod
self.deck._usn = self.maxUsn + 1 self.col._usn = self.maxUsn + 1
self.deck.save(mod=mod) self.col.save(mod=mod)
return mod return mod
# Chunked syncing # Chunked syncing
@ -176,7 +176,7 @@ select count() from notes where id not in (select distinct nid from cards)""")
def cursorForTable(self, table): def cursorForTable(self, table):
lim = self.usnLim() lim = self.usnLim()
x = self.deck.db.execute x = self.col.db.execute
d = (self.maxUsn, lim) d = (self.maxUsn, lim)
if table == "revlog": if table == "revlog":
return x(""" return x("""
@ -206,8 +206,8 @@ from notes where %s""" % d)
self.tablesLeft.pop(0) self.tablesLeft.pop(0)
self.cursor = None self.cursor = None
# if we're the client, mark the objects as having been sent # if we're the client, mark the objects as having been sent
if not self.deck.server: if not self.col.server:
self.deck.db.execute( self.col.db.execute(
"update %s set usn=? where usn=-1"%curTable, "update %s set usn=? where usn=-1"%curTable,
self.maxUsn) self.maxUsn)
buf[curTable] = rows buf[curTable] = rows
@ -231,11 +231,11 @@ from notes where %s""" % d)
cards = [] cards = []
notes = [] notes = []
groups = [] groups = []
if self.deck.server: if self.col.server:
curs = self.deck.db.execute( curs = self.col.db.execute(
"select oid, type from graves where usn >= ?", self.minUsn) "select oid, type from graves where usn >= ?", self.minUsn)
else: else:
curs = self.deck.db.execute( curs = self.col.db.execute(
"select oid, type from graves where usn = -1") "select oid, type from graves where usn = -1")
for oid, type in curs: for oid, type in curs:
if type == REM_CARD: if type == REM_CARD:
@ -244,100 +244,100 @@ from notes where %s""" % d)
notes.append(oid) notes.append(oid)
else: else:
groups.append(oid) groups.append(oid)
if not self.deck.server: if not self.col.server:
self.deck.db.execute("update graves set usn=? where usn=-1", self.col.db.execute("update graves set usn=? where usn=-1",
self.maxUsn) self.maxUsn)
return dict(cards=cards, notes=notes, groups=groups) return dict(cards=cards, notes=notes, groups=groups)
def mergeGraves(self, graves): def mergeGraves(self, graves):
# notes first, so we don't end up with duplicate graves # notes first, so we don't end up with duplicate graves
self.deck._remNotes(graves['notes']) self.col._remNotes(graves['notes'])
self.deck.remCards(graves['cards']) self.col.remCards(graves['cards'])
for oid in graves['groups']: for oid in graves['groups']:
self.deck.groups.rem(oid) self.col.groups.rem(oid)
# Models # Models
########################################################################## ##########################################################################
def getModels(self): def getModels(self):
if self.deck.server: if self.col.server:
return [m for m in self.deck.models.all() if m['usn'] >= self.minUsn] return [m for m in self.col.models.all() if m['usn'] >= self.minUsn]
else: else:
mods = [m for m in self.deck.models.all() if m['usn'] == -1] mods = [m for m in self.col.models.all() if m['usn'] == -1]
for m in mods: for m in mods:
m['usn'] = self.maxUsn m['usn'] = self.maxUsn
self.deck.models.save() self.col.models.save()
return mods return mods
def mergeModels(self, rchg): def mergeModels(self, rchg):
for r in rchg: for r in rchg:
l = self.deck.models.get(r['id']) l = self.col.models.get(r['id'])
# if missing locally or server is newer, update # if missing locally or server is newer, update
if not l or r['mod'] > l['mod']: if not l or r['mod'] > l['mod']:
self.deck.models.update(r) self.col.models.update(r)
# Groups # Groups
########################################################################## ##########################################################################
def getGroups(self): def getGroups(self):
if self.deck.server: if self.col.server:
return [ return [
[g for g in self.deck.groups.all() if g['usn'] >= self.minUsn], [g for g in self.col.groups.all() if g['usn'] >= self.minUsn],
[g for g in self.deck.groups.allConf() if g['usn'] >= self.minUsn] [g for g in self.col.groups.allConf() if g['usn'] >= self.minUsn]
] ]
else: else:
groups = [g for g in self.deck.groups.all() if g['usn'] == -1] groups = [g for g in self.col.groups.all() if g['usn'] == -1]
for g in groups: for g in groups:
g['usn'] = self.maxUsn g['usn'] = self.maxUsn
gconf = [g for g in self.deck.groups.allConf() if g['usn'] == -1] gconf = [g for g in self.col.groups.allConf() if g['usn'] == -1]
for g in gconf: for g in gconf:
g['usn'] = self.maxUsn g['usn'] = self.maxUsn
self.deck.groups.save() self.col.groups.save()
return [groups, gconf] return [groups, gconf]
def mergeGroups(self, rchg): def mergeGroups(self, rchg):
for r in rchg[0]: for r in rchg[0]:
l = self.deck.groups.get(r['id'], False) l = self.col.groups.get(r['id'], False)
# if missing locally or server is newer, update # if missing locally or server is newer, update
if not l or r['mod'] > l['mod']: if not l or r['mod'] > l['mod']:
self.deck.groups.update(r) self.col.groups.update(r)
for r in rchg[1]: for r in rchg[1]:
l = self.deck.groups.conf(r['id']) l = self.col.groups.conf(r['id'])
# if missing locally or server is newer, update # if missing locally or server is newer, update
if not l or r['mod'] > l['mod']: if not l or r['mod'] > l['mod']:
self.deck.groups.updateConf(r) self.col.groups.updateConf(r)
# Tags # Tags
########################################################################## ##########################################################################
def getTags(self): def getTags(self):
if self.deck.server: if self.col.server:
return [t for t, usn in self.deck.tags.allItems() return [t for t, usn in self.col.tags.allItems()
if usn >= self.minUsn] if usn >= self.minUsn]
else: else:
tags = [] tags = []
for t, usn in self.deck.tags.allItems(): for t, usn in self.col.tags.allItems():
if usn == -1: if usn == -1:
self.deck.tags.tags[t] = self.maxUsn self.col.tags.tags[t] = self.maxUsn
tags.append(t) tags.append(t)
self.deck.tags.save() self.col.tags.save()
return tags return tags
def mergeTags(self, tags): def mergeTags(self, tags):
self.deck.tags.register(tags, usn=self.maxUsn) self.col.tags.register(tags, usn=self.maxUsn)
# Cards/notes/revlog # Cards/notes/revlog
########################################################################## ##########################################################################
def mergeRevlog(self, logs): def mergeRevlog(self, logs):
self.deck.db.executemany( self.col.db.executemany(
"insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)", "insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)",
logs) logs)
def newerRows(self, data, table, modIdx): def newerRows(self, data, table, modIdx):
ids = (r[0] for r in data) ids = (r[0] for r in data)
lmods = {} lmods = {}
for id, mod in self.deck.db.execute( for id, mod in self.col.db.execute(
"select id, mod from %s where id in %s and %s" % ( "select id, mod from %s where id in %s and %s" % (
table, ids2str(ids), self.usnLim())): table, ids2str(ids), self.usnLim())):
lmods[id] = mod lmods[id] = mod
@ -348,26 +348,26 @@ from notes where %s""" % d)
return update return update
def mergeCards(self, cards): def mergeCards(self, cards):
self.deck.db.executemany( self.col.db.executemany(
"insert or replace into cards values " "insert or replace into cards values "
"(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
self.newerRows(cards, "cards", 4)) self.newerRows(cards, "cards", 4))
def mergeNotes(self, notes): def mergeNotes(self, notes):
rows = self.newerRows(notes, "notes", 4) rows = self.newerRows(notes, "notes", 4)
self.deck.db.executemany( self.col.db.executemany(
"insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)", "insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)",
rows) rows)
self.deck.updateFieldCache([f[0] for f in rows]) self.col.updateFieldCache([f[0] for f in rows])
# Deck config # Col config
########################################################################## ##########################################################################
def getConf(self): def getConf(self):
return self.deck.conf return self.col.conf
def mergeConf(self, conf): def mergeConf(self, conf):
self.deck.conf = conf self.col.conf = conf
# Local syncing for unit tests # Local syncing for unit tests
########################################################################## ##########################################################################
@ -375,7 +375,7 @@ from notes where %s""" % d)
class LocalServer(Syncer): class LocalServer(Syncer):
# serialize/deserialize payload, so we don't end up sharing objects # serialize/deserialize payload, so we don't end up sharing objects
# between decks # between cols
def applyChanges(self, minUsn, lnewer, changes): def applyChanges(self, minUsn, lnewer, changes):
l = simplejson.loads; d = simplejson.dumps l = simplejson.loads; d = simplejson.dumps
return l(d(Syncer.applyChanges(self, minUsn, lnewer, l(d(changes))))) return l(d(Syncer.applyChanges(self, minUsn, lnewer, l(d(changes)))))
@ -499,30 +499,30 @@ class RemoteServer(Syncer, HttpSyncer):
class FullSyncer(HttpSyncer): class FullSyncer(HttpSyncer):
def __init__(self, deck, hkey): def __init__(self, col, hkey):
self.deck = deck self.col = col
self.hkey = hkey self.hkey = hkey
def _con(self): def _con(self):
return httplib2.Http(timeout=60) return httplib2.Http(timeout=60)
def download(self): def download(self):
self.deck.close() self.col.close()
resp, cont = self._con().request( resp, cont = self._con().request(
SYNC_URL+"download?" + urllib.urlencode(self._vars())) SYNC_URL+"download?" + urllib.urlencode(self._vars()))
if resp['status'] != '200': if resp['status'] != '200':
raise Exception("Invalid response code: %s" % resp['status']) raise Exception("Invalid response code: %s" % resp['status'])
tpath = self.deck.path + ".tmp" tpath = self.col.path + ".tmp"
open(tpath, "wb").write(cont) open(tpath, "wb").write(cont)
os.unlink(self.deck.path) os.unlink(self.col.path)
os.rename(tpath, self.deck.path) os.rename(tpath, self.col.path)
d = DB(self.deck.path) d = DB(self.col.path)
assert d.scalar("pragma integrity_check") == "ok" assert d.scalar("pragma integrity_check") == "ok"
self.deck = None self.col = None
def upload(self): def upload(self):
self.deck.beforeUpload() self.col.beforeUpload()
assert self.postData(self._con(), "upload", open(self.deck.path, "rb"), assert self.postData(self._con(), "upload", open(self.col.path, "rb"),
self._vars(), comp=6) == "OK" self._vars(), comp=6) == "OK"
# Media syncing # Media syncing
@ -530,16 +530,16 @@ class FullSyncer(HttpSyncer):
class MediaSyncer(object): class MediaSyncer(object):
def __init__(self, deck, server=None): def __init__(self, col, server=None):
self.deck = deck self.col = col
self.server = server self.server = server
self.added = None self.added = None
def sync(self, mediaUsn): def sync(self, mediaUsn):
# step 1: check if there have been any changes # step 1: check if there have been any changes
self.deck.media.findChanges() self.col.media.findChanges()
lusn = self.deck.media.usn() lusn = self.col.media.usn()
if lusn == mediaUsn and not self.deck.media.hasChanged(): if lusn == mediaUsn and not self.col.media.hasChanged():
return "noChanges" return "noChanges"
# step 2: send/recv deletions # step 2: send/recv deletions
runHook("mediaSync", "remove") runHook("mediaSync", "remove")
@ -563,30 +563,30 @@ class MediaSyncer(object):
# when server has run out of files, it returns bumped usn # when server has run out of files, it returns bumped usn
break break
# step 5: finalize # step 5: finalize
self.deck.media.setUsn(usn) self.col.media.setUsn(usn)
self.deck.media.clearLog() self.col.media.clearLog()
# clear cursor so successive calls work # clear cursor so successive calls work
self.added = None self.added = None
return "success" return "success"
def removed(self): def removed(self):
return self.deck.media.removed() return self.col.media.removed()
def remove(self, fnames, minUsn=None): def remove(self, fnames, minUsn=None):
self.deck.media.syncRemove(fnames) self.col.media.syncRemove(fnames)
if minUsn is not None: if minUsn is not None:
# we're the server # we're the server
self.minUsn = minUsn self.minUsn = minUsn
return self.deck.media.removed() return self.col.media.removed()
def files(self): def files(self):
if not self.added: if not self.added:
self.added = self.deck.media.added() self.added = self.col.media.added()
return self.deck.media.zipFromAdded(self.added) return self.col.media.zipFromAdded(self.added)
def addFiles(self, zip): def addFiles(self, zip):
"True if zip is the last in set. Server returns new usn instead." "True if zip is the last in set. Server returns new usn instead."
return self.deck.media.syncAdd(zip) return self.col.media.syncAdd(zip)
# Remote media syncing # Remote media syncing
########################################################################## ##########################################################################

View file

@ -18,8 +18,8 @@ class TagManager(object):
# Registry save/load # Registry save/load
############################################################# #############################################################
def __init__(self, deck): def __init__(self, col):
self.deck = deck self.col = col
def load(self, json): def load(self, json):
self.tags = simplejson.loads(json) self.tags = simplejson.loads(json)
@ -27,7 +27,7 @@ class TagManager(object):
def flush(self): def flush(self):
if self.changed: if self.changed:
self.deck.db.execute("update deck set tags=?", self.col.db.execute("update col set tags=?",
simplejson.dumps(self.tags)) simplejson.dumps(self.tags))
# Registering and fetching tags # Registering and fetching tags
@ -39,7 +39,7 @@ class TagManager(object):
# versions of the same tag if they ignore the qt autocomplete. # versions of the same tag if they ignore the qt autocomplete.
for t in tags: for t in tags:
if t not in self.tags: if t not in self.tags:
self.tags[t] = self.deck.usn() if usn is None else usn self.tags[t] = self.col.usn() if usn is None else usn
self.changed = True self.changed = True
def all(self): def all(self):
@ -55,7 +55,7 @@ class TagManager(object):
self.tags = {} self.tags = {}
self.changed = True self.changed = True
self.register(set(self.split( self.register(set(self.split(
" ".join(self.deck.db.list("select distinct tags from notes"+lim))))) " ".join(self.col.db.list("select distinct tags from notes"+lim)))))
def allItems(self): def allItems(self):
return self.tags.items() return self.tags.items()
@ -82,7 +82,7 @@ class TagManager(object):
fn = self.remFromStr fn = self.remFromStr
lim = " or ".join( lim = " or ".join(
[l+"like :_%d" % c for c, t in enumerate(newTags)]) [l+"like :_%d" % c for c, t in enumerate(newTags)])
res = self.deck.db.all( res = self.col.db.all(
"select id, tags from notes where id in %s and %s" % ( "select id, tags from notes where id in %s and %s" % (
ids2str(ids), lim), ids2str(ids), lim),
**dict([("_%d" % x, '%% %s %%' % y) **dict([("_%d" % x, '%% %s %%' % y)
@ -92,8 +92,8 @@ class TagManager(object):
def fix(row): def fix(row):
nids.append(row[0]) nids.append(row[0])
return {'id': row[0], 't': fn(tags, row[1]), 'n':intTime(), return {'id': row[0], 't': fn(tags, row[1]), 'n':intTime(),
'u':self.deck.usn()} 'u':self.col.usn()}
self.deck.db.executemany( self.col.db.executemany(
"update notes set tags=:t,mod=:n,usn=:u where id = :id", "update notes set tags=:t,mod=:n,usn=:u where id = :id",
[fix(row) for row in res]) [fix(row) for row in res])
@ -169,10 +169,10 @@ class TagManager(object):
lim = lim2 lim = lim2
args += ['%% %s %%' % t for t in no] args += ['%% %s %%' % t for t in no]
query += " where " + lim query += " where " + lim
return self.deck.db.list(query, *args) return self.col.db.list(query, *args)
def setGroupForTags(self, yes, no, gid): def setGroupForTags(self, yes, no, gid):
nids = self.selTagNids(yes, no) nids = self.selTagNids(yes, no)
self.deck.db.execute( self.col.db.execute(
"update cards set gid=?,mod=?,usn=? where nid in "+ids2str(nids), "update cards set gid=?,mod=?,usn=? where nid in "+ids2str(nids),
gid, intTime(), self.deck.usn()) gid, intTime(), self.col.usn())

View file

@ -6,9 +6,9 @@ import os, time, simplejson, re, datetime, shutil
from anki.lang import _ from anki.lang import _
from anki.utils import intTime, tmpfile, ids2str, splitFields from anki.utils import intTime, tmpfile, ids2str, splitFields
from anki.db import DB from anki.db import DB
from anki.deck import _Deck from anki.collection import _Collection
from anki.consts import * from anki.consts import *
from anki.storage import _addSchema, _getDeckVars, _addDeckVars, \ from anki.storage import _addSchema, _getColVars, _addColVars, \
_updateIndices _updateIndices
# #
@ -31,9 +31,9 @@ class Upgrader(object):
self.path = path self.path = path
self._openDB(path) self._openDB(path)
self._upgradeSchema() self._upgradeSchema()
self._openDeck() self._openCol()
self._upgradeDeck() self._upgradeRest()
return self.deck return self.col
# Integrity checking # Integrity checking
###################################################################### ######################################################################
@ -117,8 +117,8 @@ analyze;""")
shutil.copy(path, self.tmppath) shutil.copy(path, self.tmppath)
self.db = DB(self.tmppath) self.db = DB(self.tmppath)
def _openDeck(self): def _openCol(self):
self.deck = _Deck(self.db) self.col = _Collection(self.db)
# Schema upgrade # Schema upgrade
###################################################################### ######################################################################
@ -270,7 +270,7 @@ yesCount from reviewHistory"""):
tags = {} tags = {}
for t in db.list("select tag from tags"): for t in db.list("select tag from tags"):
tags[t] = intTime() tags[t] = intTime()
db.execute("update deck set tags = ?", simplejson.dumps(tags)) db.execute("update col set tags = ?", simplejson.dumps(tags))
db.execute("drop table tags") db.execute("drop table tags")
db.execute("drop table cardTags") db.execute("drop table cardTags")
@ -282,15 +282,14 @@ yesCount from reviewHistory"""):
_updateIndices(db) _updateIndices(db)
def _migrateDeckTbl(self): def _migrateDeckTbl(self):
import anki.deck
db = self.db db = self.db
db.execute("delete from deck") db.execute("delete from col")
db.execute(""" db.execute("""
insert or replace into deck select id, cast(created as int), :t, insert or replace into col select id, cast(created as int), :t,
:t, 99, 0, 0, cast(lastSync as int), :t, 99, 0, 0, cast(lastSync as int),
"", "", "", "", "" from decks""", t=intTime()) "", "", "", "", "" from decks""", t=intTime())
# prepare a group to store the old deck options # prepare a group to store the old deck options
g, gc, conf = _getDeckVars(db) g, gc, conf = _getColVars(db)
# delete old selective study settings, which we can't auto-upgrade easily # delete old selective study settings, which we can't auto-upgrade easily
keys = ("newActive", "newInactive", "revActive", "revInactive") keys = ("newActive", "newInactive", "revActive", "revInactive")
for k in keys: for k in keys:
@ -311,7 +310,7 @@ insert or replace into deck select id, cast(created as int), :t,
pass pass
else: else:
conf[k] = v conf[k] = v
_addDeckVars(db, g, gc, conf) _addColVars(db, g, gc, conf)
# clean up # clean up
db.execute("drop table decks") db.execute("drop table decks")
db.execute("drop table deckVars") db.execute("drop table deckVars")
@ -338,7 +337,7 @@ insert or replace into deck select id, cast(created as int), :t,
mods[m['id']] = m mods[m['id']] = m
db.execute("update notes set mid = ? where mid = ?", t, row[0]) db.execute("update notes set mid = ? where mid = ?", t, row[0])
# save and clean up # save and clean up
db.execute("update deck set models = ?", simplejson.dumps(mods)) db.execute("update col set models = ?", simplejson.dumps(mods))
db.execute("drop table fieldModels") db.execute("drop table fieldModels")
db.execute("drop table cardModels") db.execute("drop table cardModels")
db.execute("drop table models") db.execute("drop table models")
@ -416,7 +415,7 @@ order by ordinal""", mid)):
# explicit on upgrade. # explicit on upgrade.
# - likewise with alignment and background color # - likewise with alignment and background color
def _upgradeTemplates(self): def _upgradeTemplates(self):
d = self.deck d = self.col
for m in d.models.all(): for m in d.models.all():
# cache field styles # cache field styles
styles = {} styles = {}
@ -470,7 +469,7 @@ order by ordinal""", mid)):
# process, we automatically convert the references to new fields. # process, we automatically convert the references to new fields.
def _rewriteMediaRefs(self): def _rewriteMediaRefs(self):
deck = self.deck col = self.col
def rewriteRef(key): def rewriteRef(key):
all, fname = match all, fname = match
if all in state['mflds']: if all in state['mflds']:
@ -487,7 +486,7 @@ order by ordinal""", mid)):
pre, ofld, suf = m2.groups() pre, ofld, suf = m2.groups()
# get index of field name # get index of field name
try: try:
idx = deck.models.fieldMap(m)[ofld][0] idx = col.models.fieldMap(m)[ofld][0]
except: except:
# invalid field or tag reference; don't rewrite # invalid field or tag reference; don't rewrite
return return
@ -495,33 +494,33 @@ order by ordinal""", mid)):
while 1: while 1:
state['fields'] += 1 state['fields'] += 1
fld = "Media %d" % state['fields'] fld = "Media %d" % state['fields']
if fld not in deck.models.fieldMap(m).keys(): if fld not in col.models.fieldMap(m).keys():
break break
# add the new field # add the new field
f = deck.models.newField(fld) f = col.models.newField(fld)
deck.models.addField(m, f) col.models.addField(m, f)
# loop through notes and write reference into new field # loop through notes and write reference into new field
data = [] data = []
for id, flds in self.deck.db.execute( for id, flds in self.col.db.execute(
"select id, flds from notes where id in "+ "select id, flds from notes where id in "+
ids2str(deck.models.nids(m))): ids2str(col.models.nids(m))):
sflds = splitFields(flds) sflds = splitFields(flds)
ref = all.replace(fname, pre+sflds[idx]+suf) ref = all.replace(fname, pre+sflds[idx]+suf)
data.append((flds+ref, id)) data.append((flds+ref, id))
# update notes # update notes
deck.db.executemany("update notes set flds=? where id=?", col.db.executemany("update notes set flds=? where id=?",
data) data)
# note field for future # note field for future
state['mflds'][fname] = fld state['mflds'][fname] = fld
new = fld new = fld
# rewrite reference in template # rewrite reference in template
t[key] = t[key].replace(all, "{{{%s}}}" % new) t[key] = t[key].replace(all, "{{{%s}}}" % new)
regexps = deck.media.regexps + ( regexps = col.media.regexps + (
r"(\[latex\](.+?)\[/latex\])", r"(\[latex\](.+?)\[/latex\])",
r"(\[\$\](.+?)\[/\$\])", r"(\[\$\](.+?)\[/\$\])",
r"(\[\$\$\](.+?)\[/\$\$\])") r"(\[\$\$\](.+?)\[/\$\$\])")
# process each model # process each model
for m in deck.models.all(): for m in col.models.all():
state = dict(mflds={}, fields=0) state = dict(mflds={}, fields=0)
for t in m['tmpls']: for t in m['tmpls']:
for r in regexps: for r in regexps:
@ -530,7 +529,7 @@ order by ordinal""", mid)):
for match in re.findall(r, t['afmt']): for match in re.findall(r, t['afmt']):
rewriteRef('afmt') rewriteRef('afmt')
if state['fields']: if state['fields']:
deck.models.save(m) col.models.save(m)
# Inactive templates # Inactive templates
###################################################################### ######################################################################
@ -538,7 +537,7 @@ order by ordinal""", mid)):
# marked inactive and have no dependent cards. # marked inactive and have no dependent cards.
def _removeInactive(self): def _removeInactive(self):
d = self.deck d = self.col
for m in d.models.all(): for m in d.models.all():
remove = [] remove = []
for t in m['tmpls']: for t in m['tmpls']:
@ -552,15 +551,14 @@ and ord = ? limit 1""", m['id'], t['ord']):
m['tmpls'].remove(t) m['tmpls'].remove(t)
d.models.save(m) d.models.save(m)
# Upgrading deck # Post-schema upgrade
###################################################################### ######################################################################
def _upgradeDeck(self): def _upgradeRest(self):
"Handle the rest of the upgrade to 2.0." "Handle the rest of the upgrade to 2.0."
import anki.deck col = self.col
deck = self.deck
# make sure we have a current model id # make sure we have a current model id
deck.models.setCurrent(deck.models.models.values()[0]) col.models.setCurrent(col.models.models.values()[0])
# remove unused templates that were marked inactive # remove unused templates that were marked inactive
self._removeInactive() self._removeInactive()
# rewrite media references in card template # rewrite media references in card template
@ -568,58 +566,58 @@ and ord = ? limit 1""", m['id'], t['ord']):
# template handling has changed # template handling has changed
self._upgradeTemplates() self._upgradeTemplates()
# set new card order # set new card order
for m in deck.models.all(): for m in col.models.all():
m['newOrder'] = deck.conf['oldNewOrder'] m['newOrder'] = col.conf['oldNewOrder']
deck.models.save(m) col.models.save(m)
del deck.conf['oldNewOrder'] del col.conf['oldNewOrder']
# fix creation time # fix creation time
deck.sched._updateCutoff() col.sched._updateCutoff()
d = datetime.datetime.today() d = datetime.datetime.today()
d -= datetime.timedelta(hours=4) d -= datetime.timedelta(hours=4)
d = datetime.datetime(d.year, d.month, d.day) d = datetime.datetime(d.year, d.month, d.day)
d += datetime.timedelta(hours=4) d += datetime.timedelta(hours=4)
d -= datetime.timedelta(days=1+int((time.time()-deck.crt)/86400)) d -= datetime.timedelta(days=1+int((time.time()-col.crt)/86400))
deck.crt = int(time.mktime(d.timetuple())) col.crt = int(time.mktime(d.timetuple()))
deck.sched._updateCutoff() col.sched._updateCutoff()
# update uniq cache # update uniq cache
deck.updateFieldCache(deck.db.list("select id from notes")) col.updateFieldCache(col.db.list("select id from notes"))
# remove old views # remove old views
for v in ("failedCards", "revCardsOld", "revCardsNew", for v in ("failedCards", "revCardsOld", "revCardsNew",
"revCardsDue", "revCardsRandom", "acqCardsRandom", "revCardsDue", "revCardsRandom", "acqCardsRandom",
"acqCardsOld", "acqCardsNew"): "acqCardsOld", "acqCardsNew"):
deck.db.execute("drop view if exists %s" % v) col.db.execute("drop view if exists %s" % v)
# remove stats, as it's all in the revlog now # remove stats, as it's all in the revlog now
deck.db.execute("drop table if exists stats") col.db.execute("drop table if exists stats")
# suspended cards don't use ranges anymore # suspended cards don't use ranges anymore
deck.db.execute("update cards set queue=-1 where queue between -3 and -1") col.db.execute("update cards set queue=-1 where queue between -3 and -1")
deck.db.execute("update cards set queue=-2 where queue between 3 and 5") col.db.execute("update cards set queue=-2 where queue between 3 and 5")
deck.db.execute("update cards set queue=-3 where queue between 6 and 8") col.db.execute("update cards set queue=-3 where queue between 6 and 8")
# remove old deleted tables # remove old deleted tables
for t in ("cards", "notes", "models", "media"): for t in ("cards", "notes", "models", "media"):
deck.db.execute("drop table if exists %sDeleted" % t) col.db.execute("drop table if exists %sDeleted" % t)
# rewrite due times for new cards # rewrite due times for new cards
deck.db.execute(""" col.db.execute("""
update cards set due = nid where type=0""") update cards set due = nid where type=0""")
# and failed cards # and failed cards
left = len(deck.groups.conf(1)['new']['delays']) left = len(col.groups.conf(1)['new']['delays'])
deck.db.execute("update cards set edue = ?, left=? where type = 1", col.db.execute("update cards set edue = ?, left=? where type = 1",
deck.sched.today+1, left) col.sched.today+1, left)
# and due cards # and due cards
deck.db.execute(""" col.db.execute("""
update cards set due = cast( update cards set due = cast(
(case when due < :stamp then 0 else 1 end) + (case when due < :stamp then 0 else 1 end) +
((due-:stamp)/86400) as int)+:today where type = 2 ((due-:stamp)/86400) as int)+:today where type = 2
""", stamp=deck.sched.dayCutoff, today=deck.sched.today) """, stamp=col.sched.dayCutoff, today=col.sched.today)
# possibly re-randomize # possibly re-randomize
if deck.models.randomNew(): if col.models.randomNew():
deck.sched.randomizeCards() col.sched.randomizeCards()
# update insertion id # update insertion id
deck.conf['nextPos'] = deck.db.scalar("select max(id) from notes")+1 col.conf['nextPos'] = col.db.scalar("select max(id) from notes")+1
deck.save() col.save()
# optimize and finish # optimize and finish
deck.db.commit() col.db.commit()
deck.db.execute("vacuum") col.db.execute("vacuum")
deck.db.execute("analyze") col.db.execute("analyze")
deck.db.execute("update deck set ver = ?", SCHEMA_VERSION) col.db.execute("update col set ver = ?", SCHEMA_VERSION)
deck.save() col.save()

View file

@ -2,7 +2,7 @@
import nose, os, tempfile import nose, os, tempfile
import anki import anki
from anki import Deck from anki import open as aopen
from anki.exporting import * from anki.exporting import *
from anki.stdmodels import * from anki.stdmodels import *
@ -12,7 +12,7 @@ testDir = os.path.dirname(__file__)
def setup1(): def setup1():
global deck global deck
deck = Deck() deck = aopen()
deck.addModel(BasicModel()) deck.addModel(BasicModel())
deck.currentModel.cardModels[1].active = True deck.currentModel.cardModels[1].active = True
f = deck.newNote() f = deck.newNote()
@ -33,14 +33,14 @@ def test_export_anki():
e.exportInto(newname) e.exportInto(newname)
assert deck.modified == oldTime assert deck.modified == oldTime
# connect to new deck # connect to new deck
d2 = Deck(newname, backup=False) d2 = aopen(newname, backup=False)
assert d2.cardCount() == 4 assert d2.cardCount() == 4
# try again, limited to a tag # try again, limited to a tag
newname = unicode(tempfile.mkstemp(prefix="ankitest")[1]) newname = unicode(tempfile.mkstemp(prefix="ankitest")[1])
os.unlink(newname) os.unlink(newname)
e.limitTags = ['tag'] e.limitTags = ['tag']
e.exportInto(newname) e.exportInto(newname)
d2 = Deck(newname, backup=False) d2 = aopen(newname, backup=False)
assert d2.cardCount() == 2 assert d2.cardCount() == 2
@nose.with_setup(setup1) @nose.with_setup(setup1)

View file

@ -1,5 +1,5 @@
import tempfile, os, shutil import tempfile, os, shutil
from anki import Deck from anki import open as aopen
def assertException(exception, func): def assertException(exception, func):
found = False found = False
@ -12,7 +12,7 @@ def assertException(exception, func):
def getEmptyDeck(**kwargs): def getEmptyDeck(**kwargs):
(fd, nam) = tempfile.mkstemp(suffix=".anki2") (fd, nam) = tempfile.mkstemp(suffix=".anki2")
os.unlink(nam) os.unlink(nam)
return Deck(nam, **kwargs) return aopen(nam, **kwargs)
def getUpgradeDeckPath(): def getUpgradeDeckPath():
src = os.path.join(testDir, "support", "anki12.anki") src = os.path.join(testDir, "support", "anki12.anki")

View file

@ -6,7 +6,7 @@ from tests.shared import assertException, getEmptyDeck, testDir, \
from anki.stdmodels import addBasicModel from anki.stdmodels import addBasicModel
from anki.consts import * from anki.consts import *
from anki import Deck from anki import open as aopen
newPath = None newPath = None
newMod = None newMod = None
@ -18,7 +18,7 @@ def test_create():
os.unlink(path) os.unlink(path)
except OSError: except OSError:
pass pass
deck = Deck(path) deck = aopen(path)
# for open() # for open()
newPath = deck.path newPath = deck.path
deck.close() deck.close()
@ -26,18 +26,18 @@ def test_create():
del deck del deck
def test_open(): def test_open():
deck = Deck(newPath) deck = aopen(newPath)
assert deck.mod == newMod assert deck.mod == newMod
deck.close() deck.close()
def test_openReadOnly(): def test_openReadOnly():
# non-writeable dir # non-writeable dir
assertException(Exception, assertException(Exception,
lambda: Deck("/attachroot.anki2")) lambda: aopen("/attachroot.anki2"))
# reuse tmp file from before, test non-writeable file # reuse tmp file from before, test non-writeable file
os.chmod(newPath, 0) os.chmod(newPath, 0)
assertException(Exception, assertException(Exception,
lambda: Deck(newPath)) lambda: aopen(newPath))
os.chmod(newPath, 0666) os.chmod(newPath, 0666)
os.unlink(newPath) os.unlink(newPath)

View file

@ -5,7 +5,6 @@ from tests.shared import assertException, getUpgradeDeckPath, getEmptyDeck
from anki.upgrade import Upgrader from anki.upgrade import Upgrader
from anki.utils import ids2str from anki.utils import ids2str
from anki.errors import * from anki.errors import *
from anki import Deck
from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \ from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \
SupermemoXmlImporter SupermemoXmlImporter
from anki.notes import Note from anki.notes import Note

View file

@ -1,7 +1,6 @@
# coding: utf-8 # coding: utf-8
import tempfile, os, time import tempfile, os, time
from anki import Deck
from anki.utils import checksum from anki.utils import checksum
from shared import getEmptyDeck, testDir from shared import getEmptyDeck, testDir

View file

@ -4,13 +4,13 @@ import nose, os, tempfile, shutil, time
from tests.shared import assertException from tests.shared import assertException
from anki.errors import * from anki.errors import *
from anki import Deck
from anki.utils import intTime from anki.utils import intTime
from anki.sync import Syncer, FullSyncer, LocalServer, RemoteServer, \ from anki.sync import Syncer, FullSyncer, LocalServer, RemoteServer, \
MediaSyncer, RemoteMediaServer MediaSyncer, RemoteMediaServer
from anki.notes import Note from anki.notes import Note
from anki.cards import Card from anki.cards import Card
from tests.shared import getEmptyDeck from tests.shared import getEmptyDeck
from anki import open as aopen
deck1=None deck1=None
deck2=None deck2=None
@ -91,7 +91,7 @@ def test_remoteSync():
lmod = ts.client.deck.mod lmod = ts.client.deck.mod
f = FullSyncer(ts.client.deck, TEST_HKEY) f = FullSyncer(ts.client.deck, TEST_HKEY)
f.download() f.download()
d = Deck(ts.client.deck.path) d = aopen(ts.client.deck.path)
assert d.mod == lmod assert d.mod == lmod
# Remote media tests # Remote media tests

View file

@ -24,8 +24,8 @@ def test_graphs_empty():
assert d.stats().report() assert d.stats().report()
def test_graphs(): def test_graphs():
from anki import Deck from anki import open as aopen
d = Deck(os.path.expanduser("~/test.anki2")) d = aopen(os.path.expanduser("~/test.anki2"))
g = d.stats() g = d.stats()
rep = g.report() rep = g.report()
open(os.path.expanduser("~/test.html"), "w").write(rep) open(os.path.expanduser("~/test.html"), "w").write(rep)

View file

@ -4,7 +4,7 @@ import nose, os, tempfile, shutil, time
from tests.shared import assertException from tests.shared import assertException
from anki.errors import * from anki.errors import *
from anki import Deck from anki import open as aopen
from anki.utils import intTime from anki.utils import intTime
from anki.sync import Syncer, FullSyncer, LocalServer, RemoteServer, \ from anki.sync import Syncer, FullSyncer, LocalServer, RemoteServer, \
MediaSyncer, RemoteMediaServer MediaSyncer, RemoteMediaServer
@ -210,7 +210,7 @@ def test_threeway():
d3path = deck1.path.replace(".anki", "2.anki") d3path = deck1.path.replace(".anki", "2.anki")
shutil.copy2(deck1.path, d3path) shutil.copy2(deck1.path, d3path)
deck1.reopen() deck1.reopen()
deck3 = Deck(d3path) deck3 = aopen(d3path)
client2 = Syncer(deck3, server) client2 = Syncer(deck3, server)
assert client2.sync() == "noChanges" assert client2.sync() == "noChanges"
# client 1 adds a card at time 1 # client 1 adds a card at time 1
@ -233,7 +233,7 @@ def test_threeway():
def _test_speed(): def _test_speed():
t = time.time() t = time.time()
deck1 = Deck(os.path.expanduser("~/rapid.anki")) deck1 = aopen(os.path.expanduser("~/rapid.anki"))
for tbl in "revlog", "cards", "notes", "graves": for tbl in "revlog", "cards", "notes", "graves":
deck1.db.execute("update %s set usn = -1 where usn != -1"%tbl) deck1.db.execute("update %s set usn = -1 where usn != -1"%tbl)
for m in deck1.models.all(): for m in deck1.models.all():