groups -> decks

This commit is contained in:
Damien Elmes 2011-11-23 19:28:09 +09:00
parent 279a942642
commit f7790275ce
22 changed files with 482 additions and 482 deletions

View file

@ -29,7 +29,7 @@ class Card(object):
else:
# to flush, set nid, ord, and due
self.id = timestampID(col.db, "cards")
self.gid = 1
self.did = 1
self.crt = intTime()
self.type = 0
self.queue = 0
@ -45,7 +45,7 @@ class Card(object):
def load(self):
(self.id,
self.nid,
self.gid,
self.did,
self.ord,
self.mod,
self.usn,
@ -73,7 +73,7 @@ insert or replace into cards values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
self.id,
self.nid,
self.gid,
self.did,
self.ord,
self.mod,
self.usn,
@ -109,7 +109,7 @@ lapses=?, left=?, edue=? where id = ?""",
def _getQA(self, reload=False):
if not self._qa or reload:
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.did, self.ord, f.stringTags(),
f.joinedFields()]
self._qa = self.col._renderQA(data)
return self._qa
@ -128,8 +128,8 @@ lapses=?, left=?, edue=? where id = ?""",
def model(self, reload=False):
return self._reviewData()[1]
def groupConf(self):
return self.col.groups.conf(self.gid)
def deckConf(self):
return self.col.decks.conf(self.did)
def template(self):
return self._reviewData()[1]['tmpls'][self.ord]
@ -140,4 +140,4 @@ lapses=?, left=?, edue=? where id = ?""",
def timeTaken(self):
"Time taken to answer card, in integer MS."
total = int((time.time() - self.timerStarted)*1000)
return min(total, self.groupConf()['maxTaken']*1000)
return min(total, self.deckConf()['maxTaken']*1000)

View file

@ -10,7 +10,7 @@ from anki.hooks import runHook, runFilter
from anki.sched import Scheduler
from anki.models import ModelManager
from anki.media import MediaManager
from anki.groups import GroupManager
from anki.decks import DeckManager
from anki.tags import TagManager
from anki.consts import *
from anki.errors import AnkiError
@ -20,9 +20,9 @@ import anki.cards, anki.notes, anki.template, anki.cram, anki.find
defaultConf = {
# scheduling options
'activeGroups': [1],
'topGroup': 1,
'curGroup': 1,
'activeDecks': [1],
'topDeck': 1,
'curDeck': 1,
'revOrder': REV_CARDS_RANDOM,
# other config
'nextPos': 1,
@ -44,7 +44,7 @@ class _Collection(object):
self.clearUndo()
self.media = MediaManager(self)
self.models = ModelManager(self)
self.groups = GroupManager(self)
self.decks = DeckManager(self)
self.tags = TagManager(self)
self.load()
if not self.crt:
@ -78,14 +78,14 @@ class _Collection(object):
self.ls,
self.conf,
models,
groups,
gconf,
decks,
dconf,
tags) = self.db.first("""
select crt, mod, scm, dty, usn, ls,
conf, models, groups, gconf, tags from col""")
conf, models, decks, dconf, tags from col""")
self.conf = simplejson.loads(self.conf)
self.models.load(models)
self.groups.load(groups, gconf)
self.decks.load(decks, dconf)
self.tags.load(tags)
def flush(self, mod=None):
@ -97,7 +97,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.crt, self.mod, self.scm, self.dty,
self._usn, self.ls, simplejson.dumps(self.conf))
self.models.flush()
self.groups.flush()
self.decks.flush()
self.tags.flush()
def save(self, name=None, mod=None):
@ -291,8 +291,8 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
data = []
ts = maxID(self.db)
now = intTime()
for nid, mid, gid, flds in self.db.execute(
"select id, mid, gid, flds from notes where id in "+snids):
for nid, mid, did, flds in self.db.execute(
"select id, mid, did, flds from notes where id in "+snids):
model = self.models.get(mid)
avail = self.models.availOrds(model, flds)
ok = []
@ -300,7 +300,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
if (nid,t['ord']) in have:
continue
if t['ord'] in avail:
data.append((ts, nid, t['gid'] or gid, t['ord'],
data.append((ts, nid, t['did'] or did, t['ord'],
now, nid))
ts += 1
# bulk update
@ -330,7 +330,7 @@ insert into cards values (?,?,?,?,?,-1,0,0,?,0,0,0,0,0,0,0,"")""",
card = anki.cards.Card(self)
card.nid = note.id
card.ord = template['ord']
card.gid = template['gid'] or note.gid
card.did = template['did'] or note.did
card.due = due
if flush:
card.flush()
@ -407,7 +407,7 @@ select id from notes where id in %s and id not in (select nid from cards)""" %
def _renderQA(self, data):
"Returns hash of id, question, answer."
# data is [cid, nid, mid, gid, ord, tags, flds]
# data is [cid, nid, mid, did, ord, tags, flds]
# unpack fields and create dict
flist = splitFields(data[6])
fields = {}
@ -416,7 +416,7 @@ select id from notes where id in %s and id not in (select nid from cards)""" %
fields[name] = flist[idx]
fields['Tags'] = data[5]
fields['Model'] = model['name']
fields['Group'] = self.groups.name(data[3])
fields['Deck'] = self.decks.name(data[3])
template = model['tmpls'][data[4]]
fields['Template'] = template['name']
# render q & a
@ -437,9 +437,9 @@ select id from notes where id in %s and id not in (select nid from cards)""" %
return d
def _qaData(self, where=""):
"Return [cid, nid, mid, gid, ord, tags, flds] db query"
"Return [cid, nid, mid, did, ord, tags, flds] db query"
return self.db.execute("""
select c.id, f.id, f.mid, c.gid, c.ord, f.tags, f.flds
select c.id, f.id, f.mid, c.did, c.ord, f.tags, f.flds
from cards c, notes f
where c.nid == f.id
%s""" % where)
@ -503,7 +503,7 @@ where c.nid == f.id
self.sched = self._stdSched
return True
def cramGroups(self, order="mod desc", min=0, max=None):
def cramDecks(self, order="mod desc", min=0, max=None):
self.stdSched()
self.sched = anki.cram.CramScheduler(self, order, min, max)

View file

@ -25,7 +25,7 @@ REV_CARDS_NEW_FIRST = 2
# removal types
REM_CARD = 0
REM_NOTE = 1
REM_GROUP = 2
REM_DECK = 2
# count display
COUNT_ANSWERED = 0

View file

@ -66,8 +66,8 @@ class CramScheduler(Scheduler):
else:
maxlim = ""
self.newQueue = self.col.db.list("""
select id from cards where gid in %s and queue = 2 and due >= %d
%s order by %s limit %d""" % (self._groupLimit(),
select id from cards where did in %s and queue = 2 and due >= %d
%s order by %s limit %d""" % (self._deckLimit(),
self.today+1+self.min,
maxlim,
self.order,

View file

@ -9,21 +9,21 @@ from anki.lang import _
# fixmes:
# - make sure users can't set grad interval < 1
# - make sure lists like new[delays] are not being shared by multiple groups
# - make sure lists like new[delays] are not being shared by multiple decks
# - make sure all children have parents (create as necessary)
# - when renaming a group, top level properties should be added or removed as
# - when renaming a deck, top level properties should be added or removed as
# appropriate
# notes:
# - it's difficult to enforce valid gids for models/notes/cards, as we
# may update the gid locally only to have it overwritten by a more recent
# change from somewhere else. to avoid this, we allow invalid gid
# references, and treat any invalid gids as the default group.
# - deletions of group config force a full sync
# - it's difficult to enforce valid dids for models/notes/cards, as we
# may update the did locally only to have it overwritten by a more recent
# change from somewhere else. to avoid this, we allow invalid did
# references, and treat any invalid dids as the default deck.
# - deletions of deck config force a full sync
# these are a cache of the current day's reviews. they may be wrong after a
# sync merge if someone reviewed from two locations
defaultGroup = {
defaultDeck = {
'newToday': [0, 0], # currentDay, count
'revToday': [0, 0],
'lrnToday': [0, 0],
@ -31,7 +31,7 @@ defaultGroup = {
'conf': 1,
}
# configuration only available to top level groups
# configuration only available to top level decks
defaultTopConf = {
'revLim': 100,
'newSpread': NEW_CARDS_DISTRIBUTE,
@ -41,7 +41,7 @@ defaultTopConf = {
'curModel': None,
}
# configuration available to all groups
# configuration available to all decks
defaultConf = {
'name': _("Default"),
'new': {
@ -78,7 +78,7 @@ defaultConf = {
'usn': 0,
}
class GroupManager(object):
class DeckManager(object):
# Registry save/load
#############################################################
@ -86,13 +86,13 @@ class GroupManager(object):
def __init__(self, col):
self.col = col
def load(self, groups, gconf):
self.groups = simplejson.loads(groups)
self.gconf = simplejson.loads(gconf)
def load(self, decks, dconf):
self.decks = simplejson.loads(decks)
self.dconf = simplejson.loads(dconf)
self.changed = False
def save(self, g=None):
"Can be called with either a group or a group configuration."
"Can be called with either a deck or a deck configuration."
if g:
g['mod'] = intTime()
g['usn'] = self.col.usn()
@ -100,86 +100,86 @@ class GroupManager(object):
def flush(self):
if self.changed:
self.col.db.execute("update col set groups=?, gconf=?",
simplejson.dumps(self.groups),
simplejson.dumps(self.gconf))
self.col.db.execute("update col set decks=?, dconf=?",
simplejson.dumps(self.decks),
simplejson.dumps(self.dconf))
# Group save/load
# Deck save/load
#############################################################
def id(self, name, create=True):
"Add a group with NAME. Reuse group if already exists. Return id as int."
for id, g in self.groups.items():
"Add a deck with NAME. Reuse deck if already exists. Return id as int."
for id, g in self.decks.items():
if g['name'].lower() == name.lower():
return int(id)
if not create:
return None
if "::" not in name:
# if it's a top level group, it gets the top level config
# if it's a top level deck, it gets the top level config
g = defaultTopConf.copy()
else:
# not top level; ensure all parents exist
g = {}
self._ensureParents(name)
for (k,v) in defaultGroup.items():
for (k,v) in defaultDeck.items():
g[k] = v
g['name'] = name
while 1:
id = intTime(1000)
if str(id) not in self.groups:
if str(id) not in self.decks:
break
g['id'] = id
self.groups[str(id)] = g
self.decks[str(id)] = g
self.save(g)
self.maybeAddToActive()
return int(id)
def rem(self, gid, cardsToo=False):
"Remove the group. If cardsToo, delete any cards inside."
assert gid != 1
if not str(gid) in self.groups:
def rem(self, did, cardsToo=False):
"Remove the deck. If cardsToo, delete any cards inside."
assert did != 1
if not str(did) in self.decks:
return
# delete children first
for name, id in self.children(gid):
for name, id in self.children(did):
self.rem(id, cardsToo)
# delete cards too?
if cardsToo:
self.col.remCards(self.cids(gid))
# delete the group and add a grave
del self.groups[str(gid)]
self.col._logRem([gid], REM_GROUP)
# ensure we have an active group
if gid in self.active():
self.select(int(self.groups.keys()[0]))
self.col.remCards(self.cids(did))
# delete the deck and add a grave
del self.decks[str(did)]
self.col._logRem([did], REM_DECK)
# ensure we have an active deck
if did in self.active():
self.select(int(self.decks.keys()[0]))
self.save()
def allNames(self):
"An unsorted list of all group names."
return [x['name'] for x in self.groups.values()]
"An unsorted list of all deck names."
return [x['name'] for x in self.decks.values()]
def all(self):
"A list of all groups."
return self.groups.values()
"A list of all decks."
return self.decks.values()
def get(self, gid, default=True):
id = str(gid)
if id in self.groups:
return self.groups[id]
def get(self, did, default=True):
id = str(did)
if id in self.decks:
return self.decks[id]
elif default:
return self.groups['1']
return self.decks['1']
def update(self, g):
"Add or update an existing group. Used for syncing and merging."
self.groups[str(g['id'])] = g
"Add or update an existing deck. Used for syncing and merging."
self.decks[str(g['id'])] = g
self.maybeAddToActive()
# mark registry changed, but don't bump mod time
self.save()
def rename(self, g, newName):
"Rename group prefix to NAME if not exists. Updates children."
"Rename deck prefix to NAME if not exists. Updates children."
# make sure target node doesn't already exist
if newName in self.allNames():
raise Exception("Group exists")
raise Exception("Deck exists")
# rename children
for grp in self.all():
if grp['name'].startswith(g['name'] + "::"):
@ -209,18 +209,18 @@ class GroupManager(object):
s += "::" + p
self.id(s)
# Group configurations
# Deck configurations
#############################################################
def allConf(self):
"A list of all group config."
return self.gconf.values()
"A list of all deck config."
return self.dconf.values()
def conf(self, gid):
return self.gconf[str(self.groups[str(gid)]['conf'])]
def conf(self, did):
return self.dconf[str(self.decks[str(did)]['conf'])]
def updateConf(self, g):
self.gconf[str(g['id'])] = g
self.dconf[str(g['id'])] = g
self.save()
def confId(self, name):
@ -228,19 +228,19 @@ class GroupManager(object):
c = copy.deepcopy(defaultConf)
while 1:
id = intTime(1000)
if str(id) not in self.gconf:
if str(id) not in self.dconf:
break
c['id'] = id
c['name'] = name
self.gconf[str(id)] = c
self.dconf[str(id)] = c
self.save(c)
return id
def remConf(self, id):
"Remove a configuration and update all groups using it."
"Remove a configuration and update all decks using it."
assert int(id) != 1
self.col.modSchema()
del self.gconf[str(id)]
del self.dconf[str(id)]
for g in self.all():
if str(g['conf']) == str(id):
g['conf'] = 1
@ -250,16 +250,16 @@ class GroupManager(object):
grp['conf'] = id
self.save(grp)
# Group utils
# Deck utils
#############################################################
def name(self, gid):
return self.get(gid)['name']
def name(self, did):
return self.get(did)['name']
def setGroup(self, cids, gid):
def setDeck(self, cids, did):
self.col.db.execute(
"update cards set gid=?,usn=?,mod=? where id in "+
ids2str(cids), gid, self.col.usn(), intTime())
"update cards set did=?,usn=?,mod=? where id in "+
ids2str(cids), did, self.col.usn(), intTime())
def maybeAddToActive(self):
@ -268,59 +268,59 @@ class GroupManager(object):
def sendHome(self, cids):
self.col.db.execute("""
update cards set gid=(select gid from notes f where f.id=nid),
update cards set did=(select did from notes f where f.id=nid),
usn=?,mod=? where id in %s""" % ids2str(cids),
self.col.usn(), intTime(), gid)
self.col.usn(), intTime(), did)
def cids(self, gid):
return self.col.db.list("select id from cards where gid=?", gid)
def cids(self, did):
return self.col.db.list("select id from cards where did=?", did)
# Group selection
# Deck selection
#############################################################
def top(self):
"The current top level group as an object."
g = self.get(self.col.conf['topGroup'])
"The current top level deck as an object."
g = self.get(self.col.conf['topDeck'])
return g
def active(self):
"The currrently active gids."
return self.col.conf['activeGroups']
"The currrently active dids."
return self.col.conf['activeDecks']
def selected(self):
"The currently selected gid."
return self.col.conf['curGroup']
"The currently selected did."
return self.col.conf['curDeck']
def current(self):
return self.get(self.selected())
def select(self, gid):
def select(self, did):
"Select a new branch."
# save the top level group
name = self.groups[str(gid)]['name']
self.col.conf['topGroup'] = self._topFor(name)
# current group
self.col.conf['curGroup'] = gid
# and active groups (current + all children)
actv = self.children(gid)
# save the top level deck
name = self.decks[str(did)]['name']
self.col.conf['topDeck'] = self._topFor(name)
# current deck
self.col.conf['curDeck'] = did
# and active decks (current + all children)
actv = self.children(did)
actv.sort()
self.col.conf['activeGroups'] = [gid] + [a[1] for a in actv]
self.col.conf['activeDecks'] = [did] + [a[1] for a in actv]
def children(self, gid):
"All children of gid, as (name, id)."
name = self.get(gid)['name']
def children(self, did):
"All children of did, as (name, id)."
name = self.get(did)['name']
actv = []
for g in self.all():
if g['name'].startswith(name + "::"):
actv.append((g['name'], g['id']))
return actv
def parents(self, gid):
"All parents of gid."
path = self.get(gid)['name'].split("::")
def parents(self, did):
"All parents of did."
path = self.get(did)['name'].split("::")
return [self.get(x) for x in path[:-1]]
def _topFor(self, name):
"The top level gid for NAME."
"The top level did for NAME."
path = name.split("::")
return self.id(path[0])

View file

@ -13,7 +13,7 @@ SEARCH_NID = 3
SEARCH_TEMPLATE = 4
SEARCH_FIELD = 5
SEARCH_MODEL = 6
SEARCH_GROUP = 7
SEARCH_DECK = 7
# Tools
##########################################################################
@ -121,8 +121,8 @@ order by %s""" % (lim, sort)
self._findField(token, isNeg)
elif type == SEARCH_MODEL:
self._findModel(token, isNeg)
elif type == SEARCH_GROUP:
self._findGroup(token, isNeg)
elif type == SEARCH_DECK:
self._findDeck(token, isNeg)
else:
self._findText(token, isNeg, c)
@ -191,10 +191,10 @@ order by %s""" % (lim, sort)
ids.append(m['id'])
self.lims['note'].append("mid %s in %s" % (extra, ids2str(ids)))
def _findGroup(self, val, isNeg):
def _findDeck(self, val, isNeg):
extra = "!" if isNeg else ""
id = self.col.groups.id(val, create=False) or 0
self.lims['card'].append("c.gid %s= %s" % (extra, id))
id = self.col.decks.id(val, create=False) or 0
self.lims['card'].append("c.did %s= %s" % (extra, id))
def _findTemplate(self, val, isNeg):
lims = []
@ -329,9 +329,9 @@ where mid in %s and flds like ? escape '\\'""" % (
elif token['value'].startswith("model:"):
token['value'] = token['value'][6:].lower()
type = SEARCH_MODEL
elif token['value'].startswith("group:"):
token['value'] = token['value'][6:].lower()
type = SEARCH_GROUP
elif token['value'].startswith("deck:"):
token['value'] = token['value'][5:].lower()
type = SEARCH_DECK
elif token['value'].startswith("nid:") and len(token['value']) > 4:
dec = token['value'][4:]
try:

View file

@ -27,7 +27,7 @@ class Anki1Importer(Anki2Importer):
# merge
deck.close()
mdir = self.file.replace(".anki", ".media")
self.groupPrefix = os.path.basename(self.file).replace(".anki", "")
self.deckPrefix = os.path.basename(self.file).replace(".anki", "")
self.file = deck.path
Anki2Importer.run(self, mdir)

View file

@ -14,13 +14,13 @@ from anki.importing.base import Importer
# - compare notes by guid
# - compare models by schema signature
# - compare cards by note guid + ordinal
# - compare groups by name
# - compare decks by name
#
class Anki2Importer(Importer):
needMapper = False
groupPrefix = None
deckPrefix = None
needCards = True
def run(self, media=None):
@ -38,10 +38,10 @@ class Anki2Importer(Importer):
self.src = Collection(self.file, queue=False)
def _import(self):
self._groups = {}
if self.groupPrefix:
id = self.dst.groups.id(self.groupPrefix)
self.dst.groups.select(id)
self._decks = {}
if self.deckPrefix:
id = self.dst.decks.id(self.deckPrefix)
self.dst.decks.select(id)
self._prepareTS()
self._prepareModels()
self._importNotes()
@ -76,7 +76,7 @@ class Anki2Importer(Importer):
# rewrite internal ids, models, etc
note[0] = self.ts()
note[2] = lmid
note[3] = self._gid(note[3])
note[3] = self._did(note[3])
note[4] = intTime()
note[5] = -1 # usn
add.append(note)
@ -136,27 +136,27 @@ class Anki2Importer(Importer):
else:
dst['vers'] = [src['id']]
# Groups
# Decks
######################################################################
def _gid(self, gid):
"Given gid in src col, return local id."
def _did(self, did):
"Given did in src col, return local id."
# already converted?
if gid in self._groups:
return self._groups[gid]
if did in self._decks:
return self._decks[did]
# get the name in src
g = self.src.groups.get(gid)
g = self.src.decks.get(did)
name = g['name']
# if there's a prefix, replace the top level group
if self.groupPrefix:
# if there's a prefix, replace the top level deck
if self.deckPrefix:
tmpname = "::".join(name.split("::")[1:])
name = self.groupPrefix
name = self.deckPrefix
if tmpname:
name += "::" + name
# create in local
newid = self.dst.groups.id(name)
# add to group map and return
self._groups[gid] = newid
newid = self.dst.decks.id(name)
# add to deck map and return
self._decks[did] = newid
return newid
# Cards
@ -199,7 +199,7 @@ class Anki2Importer(Importer):
# update cid, nid, etc
card[0] = self.ts()
card[1] = self._notes[guid][0]
card[2] = self._gid(card[2])
card[2] = self._did(card[2])
card[4] = intTime()
cards.append(card)
# we need to import revlog, rewriting card ids

View file

@ -15,7 +15,7 @@ from anki.consts import *
defaultModel = {
'sortf': 0,
'gid': 1,
'did': 1,
'clozectx': False,
'newOrder': NEW_CARDS_DUE,
'latexPre': """\
@ -52,7 +52,7 @@ defaultTemplate = {
'qfmt': "",
'afmt': "",
'typeAns': None,
'gid': None,
'did': None,
}
class ModelManager(object):
@ -90,16 +90,16 @@ class ModelManager(object):
def current(self):
"Get current model."
try:
m = self.get(self.col.groups.top()['curModel'])
m = self.get(self.col.decks.top()['curModel'])
assert m
return m
except:
return self.models.values()[0]
def setCurrent(self, m):
t = self.col.groups.top()
t = self.col.decks.top()
t['curModel'] = m['id']
self.col.groups.save(t)
self.col.decks.save(t)
def get(self, id):
"Get model with ID, or None."

View file

@ -19,7 +19,7 @@ class Note(object):
self.id = timestampID(col.db, "notes")
self.guid = guid64()
self._model = model
self.gid = model['gid']
self.did = model['did']
self.mid = model['id']
self.tags = []
self.fields = [""] * len(self._model['flds'])
@ -30,14 +30,14 @@ class Note(object):
def load(self):
(self.guid,
self.mid,
self.gid,
self.did,
self.mod,
self.usn,
self.tags,
self.fields,
self.flags,
self.data) = self.col.db.first("""
select guid, mid, gid, mod, usn, tags, flds, flags, data
select guid, mid, did, mod, usn, tags, flds, flags, data
from notes where id = ?""", self.id)
self.fields = splitFields(self.fields)
self.tags = self.col.tags.split(self.tags)
@ -53,7 +53,7 @@ from notes where id = ?""", self.id)
tags = self.stringTags()
res = self.col.db.execute("""
insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
self.id, self.guid, self.mid, self.gid,
self.id, self.guid, self.mid, self.did,
self.mod, self.usn, tags,
self.joinedFields(), sfld, self.flags, self.data)
self.id = res.lastrowid
@ -84,10 +84,10 @@ insert or replace into notes values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
def model(self):
return self._model
def updateCardGids(self):
def updateCardDids(self):
for c in self.cards():
if c.gid != self.gid and not c.template()['gid']:
c.gid = self.gid
if c.did != self.did and not c.template()['did']:
c.did = self.did
c.flush()
# Dict interface

View file

@ -22,7 +22,7 @@ class Scheduler(object):
self.col = col
self.queueLimit = 50
self.reportLimit = 1000
# fixme: replace reps with group based counts
# fixme: replace reps with deck based counts
self.reps = 0
self._updateCutoff()
@ -78,10 +78,10 @@ class Scheduler(object):
"Return counts over next DAYS. Includes today."
daysd = dict(self.col.db.all("""
select due, count() from cards
where gid in %s and queue = 2
where did in %s and queue = 2
and due between ? and ?
group by due
order by due""" % self._groupLimit(),
order by due""" % self._deckLimit(),
self.today,
self.today+days-1))
for d in range(days):
@ -111,34 +111,34 @@ order by due""" % self._groupLimit(),
def _updateStats(self, card, type, cnt=1):
key = type+"Today"
for g in ([self.col.groups.get(card.gid)] +
self.col.groups.parents(card.gid)):
for g in ([self.col.decks.get(card.did)] +
self.col.decks.parents(card.did)):
# add
g[key][1] += cnt
self.col.groups.save(g)
self.col.decks.save(g)
# Group counts
# Deck counts
##########################################################################
def groupCounts(self):
"Returns [groupname, gid, hasDue, hasNew]"
# find groups with 1 or more due cards
gids = {}
for g in self.col.groups.all():
hasDue = self._groupHasLrn(g['id']) or self._groupHasRev(g['id'])
hasNew = self._groupHasNew(g['id'])
gids[g['id']] = [hasDue or 0, hasNew or 0]
return [[grp['name'], int(gid)]+gids[int(gid)] #.get(int(gid))
for (gid, grp) in self.col.groups.groups.items()]
def deckCounts(self):
"Returns [deckname, did, hasDue, hasNew]"
# find decks with 1 or more due cards
dids = {}
for g in self.col.decks.all():
hasDue = self._deckHasLrn(g['id']) or self._deckHasRev(g['id'])
hasNew = self._deckHasNew(g['id'])
dids[g['id']] = [hasDue or 0, hasNew or 0]
return [[grp['name'], int(did)]+dids[int(did)] #.get(int(did))
for (did, grp) in self.col.decks.decks.items()]
def groupCountTree(self):
return self._groupChildren(self.groupCounts())
def deckCountTree(self):
return self._groupChildren(self.deckCounts())
def groupTree(self):
def deckTree(self):
"Like the count tree without the counts. Faster."
return self._groupChildren(
[[grp['name'], int(gid), 0, 0, 0]
for (gid, grp) in self.col.groups.groups.items()])
[[grp['name'], int(did), 0, 0, 0]
for (did, grp) in self.col.decks.decks.items()])
def _groupChildren(self, grps):
# first, split the group names into components
@ -156,14 +156,14 @@ order by due""" % self._groupLimit(),
return grp[0][0]
for (head, tail) in itertools.groupby(grps, key=key):
tail = list(tail)
gid = None
did = None
rev = 0
new = 0
children = []
for c in tail:
if len(c[0]) == 1:
# current node
gid = c[1]
did = c[1]
rev += c[2]
new += c[3]
else:
@ -175,7 +175,7 @@ order by due""" % self._groupLimit(),
for ch in children:
rev += ch[2]
new += ch[3]
tree.append((head, gid, rev, new, children))
tree.append((head, did, rev, new, children))
return tuple(tree)
# Getting the next card
@ -207,35 +207,35 @@ order by due""" % self._groupLimit(),
def _resetNewCount(self):
self.newCount = 0
pcounts = {}
# for each of the active groups
for gid in self.col.groups.active():
# get the individual group's limit
lim = self._groupNewLimitSingle(self.col.groups.get(gid))
# for each of the active decks
for did in self.col.decks.active():
# get the individual deck's limit
lim = self._deckNewLimitSingle(self.col.decks.get(did))
if not lim:
continue
# check the parents
parents = self.col.groups.parents(gid)
parents = self.col.decks.parents(did)
for p in parents:
# add if missing
if p['id'] not in pcounts:
pcounts[p['id']] = self._groupNewLimitSingle(p)
pcounts[p['id']] = self._deckNewLimitSingle(p)
# take minimum of child and parent
lim = min(pcounts[p['id']], lim)
# see how many cards we actually have
cnt = self.col.db.scalar("""
select count() from (select 1 from cards where
gid = ? and queue = 0 limit ?)""", gid, lim)
did = ? and queue = 0 limit ?)""", did, lim)
# if non-zero, decrement from parent counts
for p in parents:
pcounts[p['id']] -= cnt
# we may also be a parent
pcounts[gid] = lim - cnt
pcounts[did] = lim - cnt
# and add to running total
self.newCount += cnt
def _resetNew(self):
self._resetNewCount()
self.newGids = self.col.groups.active()
self.newDids = self.col.decks.active()
self._newQueue = []
self._updateNewCardRatio()
@ -244,25 +244,25 @@ gid = ? and queue = 0 limit ?)""", gid, lim)
return True
if not self.newCount:
return False
while self.newGids:
gid = self.newGids[0]
lim = min(self.queueLimit, self._groupNewLimit(gid))
while self.newDids:
did = self.newDids[0]
lim = min(self.queueLimit, self._deckNewLimit(did))
if lim:
# fill the queue with the current gid
# fill the queue with the current did
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 did = ? and queue = 0 limit ?""", did, lim)
if self._newQueue:
self._newQueue.reverse()
return True
# nothing left in the group; move to next
self.newGids.pop(0)
# nothing left in the deck; move to next
self.newDids.pop(0)
def _getNewCard(self):
if not self._fillNew():
return
(id, due) = self._newQueue.pop()
# move any siblings to the end?
conf = self.col.groups.conf(self.newGids[0])
conf = self.col.decks.conf(self.newDids[0])
if conf['new']['order'] == NEW_TODAY_ORD:
n = len(self._newQueue)
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
def _updateNewCardRatio(self):
if self.col.groups.top()['newSpread'] == NEW_CARDS_DISTRIBUTE:
if self.col.decks.top()['newSpread'] == NEW_CARDS_DISTRIBUTE:
if self.newCount:
self.newCardModulus = (
(self.newCount + self.revCount) / self.newCount)
@ -289,33 +289,33 @@ 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."
if not self.newCount:
return False
if self.col.groups.top()['newSpread'] == NEW_CARDS_LAST:
if self.col.decks.top()['newSpread'] == NEW_CARDS_LAST:
return False
elif self.col.groups.top()['newSpread'] == NEW_CARDS_FIRST:
elif self.col.decks.top()['newSpread'] == NEW_CARDS_FIRST:
return True
elif self.newCardModulus:
return self.reps and self.reps % self.newCardModulus == 0
def _groupHasNew(self, gid):
if not self._groupNewLimit(gid):
def _deckHasNew(self, did):
if not self._deckNewLimit(did):
return False
return self.col.db.scalar(
"select 1 from cards where gid = ? and queue = 0 limit 1", gid)
"select 1 from cards where did = ? and queue = 0 limit 1", did)
def _groupNewLimit(self, gid):
sel = self.col.groups.get(gid)
def _deckNewLimit(self, did):
sel = self.col.decks.get(did)
lim = -1
# for the group and each of its parents
for g in [sel] + self.col.groups.parents(gid):
rem = self._groupNewLimitSingle(g)
# for the deck and each of its parents
for g in [sel] + self.col.decks.parents(did):
rem = self._deckNewLimitSingle(g)
if lim == -1:
lim = rem
else:
lim = min(rem, lim)
return lim
def _groupNewLimitSingle(self, g):
c = self.col.groups.conf(g['id'])
def _deckNewLimitSingle(self, g):
c = self.col.decks.conf(g['id'])
return max(0, c['new']['perDay'] - g['newToday'][1])
# Learning queue
@ -324,8 +324,8 @@ select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
def _resetLrnCount(self):
(self.lrnCount, self.lrnRepCount) = self.col.db.first("""
select count(), sum(left) from (select left from cards where
gid in %s and queue = 1 and due < ? limit %d)""" % (
self._groupLimit(), self.reportLimit),
did in %s and queue = 1 and due < ? limit %d)""" % (
self._deckLimit(), self.reportLimit),
self.dayCutoff)
self.lrnRepCount = self.lrnRepCount or 0
@ -340,9 +340,9 @@ gid in %s and queue = 1 and due < ? limit %d)""" % (
return True
self._lrnQueue = self.col.db.all("""
select due, id from cards where
gid in %s and queue = 1 and due < :lim
limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
# as it arrives sorted by gid first, we need to sort it
did in %s and queue = 1 and due < :lim
limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff)
# as it arrives sorted by did first, we need to sort it
self._lrnQueue.sort()
return self._lrnQueue
@ -350,7 +350,7 @@ limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
if self._fillLrn():
cutoff = time.time()
if collapse:
cutoff += self.col.groups.top()['collapseTime']
cutoff += self.col.decks.top()['collapseTime']
if self._lrnQueue[0][0] < cutoff:
id = heappop(self._lrnQueue)[1]
self.lrnCount -= 1
@ -459,29 +459,29 @@ where queue = 1 and type = 2
%s
""" % (intTime(), self.col.usn(), extra))
def _groupHasLrn(self, gid):
def _deckHasLrn(self, did):
return self.col.db.scalar(
"select 1 from cards where gid = ? and queue = 1 "
"select 1 from cards where did = ? and queue = 1 "
"and due < ? limit 1",
gid, intTime() + self.col.groups.top()['collapseTime'])
did, intTime() + self.col.decks.top()['collapseTime'])
# Reviews
##########################################################################
def _groupHasRev(self, gid):
def _deckHasRev(self, did):
return self.col.db.scalar(
"select 1 from cards where gid = ? and queue = 2 "
"select 1 from cards where did = ? and queue = 2 "
"and due <= ? limit 1",
gid, self.today)
did, self.today)
def _resetRevCount(self):
top = self.col.groups.top()
top = self.col.decks.top()
lim = min(self.reportLimit,
max(0, top['revLim'] - top['revToday'][1]))
self.revCount = self.col.db.scalar("""
select count() from (select id from cards where
gid in %s and queue = 2 and due <= :day limit %d)""" % (
self._groupLimit(), lim), day=self.today)
did in %s and queue = 2 and due <= :day limit %d)""" % (
self._deckLimit(), lim), day=self.today)
def _resetRev(self):
self._resetRevCount()
@ -494,8 +494,8 @@ gid in %s and queue = 2 and due <= :day limit %d)""" % (
return True
self._revQueue = self.col.db.list("""
select id from cards where
gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
self._groupLimit(), self._revOrder(), self.queueLimit),
did in %s and queue = 2 and due <= :lim %s limit %d""" % (
self._deckLimit(), self._revOrder(), self.queueLimit),
lim=self.today)
if not self.col.conf['revOrder']:
r = random.Random()
@ -653,10 +653,10 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
##########################################################################
def _cardConf(self, card):
return self.col.groups.conf(card.gid)
return self.col.decks.conf(card.did)
def _groupLimit(self):
return ids2str(self.col.groups.active())
def _deckLimit(self):
return ids2str(self.col.decks.active())
# Daily cutoff
##########################################################################
@ -666,7 +666,7 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
self.today = int((time.time() - self.col.crt) / 86400)
# end of day cutoff
self.dayCutoff = self.col.crt + (self.today+1)*86400
# update all selected groups
# update all selected decks
def update(g):
save = False
for t in "new", "rev", "lrn", "time":
@ -675,11 +675,11 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
save = True
g[key] = [self.today, 0]
if save:
self.col.groups.save(g)
for gid in self.col.groups.active():
update(self.col.groups.get(gid))
self.col.decks.save(g)
for did in self.col.decks.active():
update(self.col.decks.get(did))
# update parents too
for grp in self.col.groups.parents(self.col.groups.selected()):
for grp in self.col.decks.parents(self.col.decks.selected()):
update(grp)
def _checkDay(self):
@ -713,15 +713,15 @@ your short-term review workload will become."""))
def revDue(self):
"True if there are any rev cards due."
return self.col.db.scalar(
("select 1 from cards where gid in %s and queue = 2 "
"and due <= ? limit 1") % self._groupLimit(),
("select 1 from cards where did in %s and queue = 2 "
"and due <= ? limit 1") % self._deckLimit(),
self.today)
def newDue(self):
"True if there are any new cards due."
return self.col.db.scalar(
("select 1 from cards where gid in %s and queue = 0 "
"limit 1") % self._groupLimit())
("select 1 from cards where did in %s and queue = 0 "
"limit 1") % self._deckLimit())
# Next time reports
##########################################################################

View file

@ -49,8 +49,8 @@ class CardStats(object):
self.addLine(_("Position"), c.due)
self.addLine(_("Model"), c.model()['name'])
self.addLine(_("Template"), c.template()['name'])
self.addLine(_("Current Group"), self.col.groups.name(c.gid))
self.addLine(_("Home Group"), self.col.groups.name(c.note().gid))
self.addLine(_("Current Deck"), self.col.decks.name(c.did))
self.addLine(_("Original Deck"), self.col.decks.name(c.note().did))
self.txt += "</table>"
return self.txt
@ -178,7 +178,7 @@ 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) -- mtr
from cards
where gid in %s and queue = 2
where did in %s and queue = 2
%s
group by day order by day""" % (self._limit(), lim),
today=self.col.sched.today,
@ -391,11 +391,11 @@ group by day order by day)""" % lim,
chunk = 30; lim = ""
data = [self.col.db.all("""
select ivl / :chunk as grp, count() from cards
where gid in %s and queue = 2 %s
where did in %s and queue = 2 %s
group by grp
order by grp""" % (self._limit(), lim), chunk=chunk)]
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 did in %s and queue = 2""" %
self._limit()))
# Eases
@ -539,7 +539,7 @@ group by hour having count() > 30 order by hour""" % lim,
i = []
(c, f) = self.col.db.first("""
select count(id), count(distinct nid) from cards
where gid in %s """ % self._limit())
where did in %s """ % self._limit())
self._line(i, _("Total cards"), c)
self._line(i, _("Total notes"), f)
(low, avg, high) = self._factors()
@ -548,7 +548,7 @@ where gid in %s """ % self._limit())
self._line(i, _("Average ease factor"), "%d%%" % avg)
self._line(i, _("Highest ease factor"), "%d%%" % high)
min = self.col.db.scalar(
"select min(id) from cards where gid in %s " % self._limit())
"select min(id) from cards where did in %s " % self._limit())
if min:
self._line(i, _("First card created"), _("%s ago") % fmtTimeSpan(
time.time() - (min/1000)))
@ -579,7 +579,7 @@ select
min(factor) / 10.0,
avg(factor) / 10.0,
max(factor) / 10.0
from cards where gid in %s and queue = 2""" % self._limit())
from cards where did in %s and queue = 2""" % self._limit())
def _cards(self):
return self.col.db.first("""
@ -588,7 +588,7 @@ 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=0 then 1 else 0 end), -- new
sum(case when queue=-1 then 1 else 0 end) -- susp
from cards where gid in %s""" % self._limit())
from cards where did in %s""" % self._limit())
# Tools
######################################################################
@ -668,11 +668,11 @@ $(function () {
data=simplejson.dumps(data), conf=simplejson.dumps(conf)))
def _limit(self):
return self.col.sched._groupLimit()
return self.col.sched._deckLimit()
def _revlogLimit(self):
return ("cid in (select id from cards where gid in %s)" %
ids2str(self.col.groups.active()))
return ("cid in (select id from cards where did in %s)" %
ids2str(self.col.decks.active()))
def _title(self, title, subtitle=""):
return '<h1>%s</h1>%s' % (title, subtitle)

View file

@ -75,8 +75,8 @@ create table if not exists col (
ls integer not null,
conf text not null,
models text not null,
groups text not null,
gconf text not null,
decks text not null,
dconf text not null,
tags text not null
);
@ -84,7 +84,7 @@ create table if not exists notes (
id integer primary key,
guid integer not null,
mid integer not null,
gid integer not null,
did integer not null,
mod integer not null,
usn integer not null,
tags text not null,
@ -102,7 +102,7 @@ create table if not exists nsums (
create table if not exists cards (
id integer primary key,
nid integer not null,
gid integer not null,
did integer not null,
ord integer not null,
mod integer not null,
usn integer not null,
@ -145,21 +145,21 @@ values(1,0,0,0,%(v)s,0,0,0,'','{}','','','{}');
def _getColVars(db):
import anki.collection
import anki.groups
g = anki.groups.defaultGroup.copy()
for k,v in anki.groups.defaultTopConf.items():
import anki.decks
g = anki.decks.defaultDeck.copy()
for k,v in anki.decks.defaultTopConf.items():
g[k] = v
g['id'] = 1
g['name'] = _("Default")
g['conf'] = 1
g['mod'] = intTime()
gc = anki.groups.defaultConf.copy()
gc = anki.decks.defaultConf.copy()
gc['id'] = 1
return g, gc, anki.collection.defaultConf.copy()
def _addColVars(db, g, gc, c):
db.execute("""
update col set conf = ?, groups = ?, gconf = ?""",
update col set conf = ?, decks = ?, dconf = ?""",
simplejson.dumps(c),
simplejson.dumps({'1': g}),
simplejson.dumps({'1': gc}))
@ -173,8 +173,8 @@ create index if not exists ix_cards_usn on cards (usn);
create index if not exists ix_revlog_usn on revlog (usn);
-- card spacing, etc
create index if not exists ix_cards_nid on cards (nid);
-- scheduling and group limiting
create index if not exists ix_cards_sched on cards (gid, queue, due);
-- scheduling and deck limiting
create index if not exists ix_cards_sched on cards (did, queue, due);
-- revlog by card
create index if not exists ix_revlog_cid on revlog (cid);
-- field uniqueness check

View file

@ -95,7 +95,7 @@ class Syncer(object):
def changes(self):
"Bundle up deletions and small objects, and apply if server."
d = dict(models=self.getModels(),
groups=self.getGroups(),
decks=self.getDecks(),
tags=self.getTags(),
graves=self.getGraves())
if self.lnewer:
@ -118,7 +118,7 @@ class Syncer(object):
self.mergeGraves(rchg['graves'])
# then the other objects
self.mergeModels(rchg['models'])
self.mergeGroups(rchg['groups'])
self.mergeDecks(rchg['decks'])
self.mergeTags(rchg['tags'])
if 'conf' in rchg:
self.mergeConf(rchg['conf'])
@ -134,7 +134,7 @@ select count() from notes where id not in (select distinct nid from cards)""")
for t in "cards", "notes", "revlog", "graves":
assert not self.col.db.scalar(
"select count() from %s where usn = -1" % t)
for g in self.col.groups.all():
for g in self.col.decks.all():
assert g['usn'] != -1
for t, usn in self.col.tags.allItems():
assert usn != -1
@ -148,8 +148,8 @@ select count() from notes where id not in (select distinct nid from cards)""")
self.col.db.scalar("select count() from graves"),
len(self.col.models.all()),
len(self.col.tags.all()),
len(self.col.groups.all()),
len(self.col.groups.allConf()),
len(self.col.decks.all()),
len(self.col.decks.allConf()),
]
def usnLim(self):
@ -184,11 +184,11 @@ select id, cid, %d, ease, ivl, lastIvl, factor, time, type
from revlog where %s""" % d)
elif table == "cards":
return x("""
select id, nid, gid, ord, mod, %d, type, queue, due, ivl, factor, reps,
select id, nid, did, ord, mod, %d, type, queue, due, ivl, factor, reps,
lapses, left, edue, flags, data from cards where %s""" % d)
else:
return x("""
select id, guid, mid, gid, mod, %d, tags, flds, '', flags, data
select id, guid, mid, did, mod, %d, tags, flds, '', flags, data
from notes where %s""" % d)
def chunk(self):
@ -230,7 +230,7 @@ from notes where %s""" % d)
def getGraves(self):
cards = []
notes = []
groups = []
decks = []
if self.col.server:
curs = self.col.db.execute(
"select oid, type from graves where usn >= ?", self.minUsn)
@ -243,18 +243,18 @@ from notes where %s""" % d)
elif type == REM_NOTE:
notes.append(oid)
else:
groups.append(oid)
decks.append(oid)
if not self.col.server:
self.col.db.execute("update graves set usn=? where usn=-1",
self.maxUsn)
return dict(cards=cards, notes=notes, groups=groups)
return dict(cards=cards, notes=notes, decks=decks)
def mergeGraves(self, graves):
# notes first, so we don't end up with duplicate graves
self.col._remNotes(graves['notes'])
self.col.remCards(graves['cards'])
for oid in graves['groups']:
self.col.groups.rem(oid)
for oid in graves['decks']:
self.col.decks.rem(oid)
# Models
##########################################################################
@ -276,36 +276,36 @@ from notes where %s""" % d)
if not l or r['mod'] > l['mod']:
self.col.models.update(r)
# Groups
# Decks
##########################################################################
def getGroups(self):
def getDecks(self):
if self.col.server:
return [
[g for g in self.col.groups.all() if g['usn'] >= self.minUsn],
[g for g in self.col.groups.allConf() if g['usn'] >= self.minUsn]
[g for g in self.col.decks.all() if g['usn'] >= self.minUsn],
[g for g in self.col.decks.allConf() if g['usn'] >= self.minUsn]
]
else:
groups = [g for g in self.col.groups.all() if g['usn'] == -1]
for g in groups:
decks = [g for g in self.col.decks.all() if g['usn'] == -1]
for g in decks:
g['usn'] = self.maxUsn
gconf = [g for g in self.col.groups.allConf() if g['usn'] == -1]
for g in gconf:
dconf = [g for g in self.col.decks.allConf() if g['usn'] == -1]
for g in dconf:
g['usn'] = self.maxUsn
self.col.groups.save()
return [groups, gconf]
self.col.decks.save()
return [decks, dconf]
def mergeGroups(self, rchg):
def mergeDecks(self, rchg):
for r in rchg[0]:
l = self.col.groups.get(r['id'], False)
l = self.col.decks.get(r['id'], False)
# if missing locally or server is newer, update
if not l or r['mod'] > l['mod']:
self.col.groups.update(r)
self.col.decks.update(r)
for r in rchg[1]:
l = self.col.groups.conf(r['id'])
l = self.col.decks.conf(r['id'])
# if missing locally or server is newer, update
if not l or r['mod'] > l['mod']:
self.col.groups.updateConf(r)
self.col.decks.updateConf(r)
# Tags
##########################################################################

View file

@ -171,8 +171,8 @@ class TagManager(object):
query += " where " + lim
return self.col.db.list(query, *args)
def setGroupForTags(self, yes, no, gid):
def setDeckForTags(self, yes, no, did):
nids = self.selTagNids(yes, no)
self.col.db.execute(
"update cards set gid=?,mod=?,usn=? where nid in "+ids2str(nids),
gid, intTime(), self.col.usn())
"update cards set did=?,mod=?,usn=? where nid in "+ids2str(nids),
did, intTime(), self.col.usn())

View file

@ -288,7 +288,7 @@ yesCount from reviewHistory"""):
insert or replace into col select id, cast(created as int), :t,
:t, 99, 0, 0, cast(lastSync as int),
"", "", "", "", "" from decks""", t=intTime())
# prepare a group to store the old deck options
# prepare a deck to store the old deck options
g, gc, conf = _getColVars(db)
# delete old selective study settings, which we can't auto-upgrade easily
keys = ("newActive", "newInactive", "revActive", "revInactive")
@ -599,7 +599,7 @@ and ord = ? limit 1""", m['id'], t['ord']):
col.db.execute("""
update cards set due = nid where type=0""")
# and failed cards
left = len(col.groups.conf(1)['new']['delays'])
left = len(col.decks.conf(1)['new']['delays'])
col.db.execute("update cards set edue = ?, left=? where type = 1",
col.sched.today+1, left)
# and due cards

View file

@ -144,10 +144,10 @@ def test_selective():
assert len(deck.tags.selTagNids(["two", "three"], [])) == 3
assert len(deck.tags.selTagNids(["two", "three"], ["one"])) == 1
assert len(deck.tags.selTagNids(["one", "three"], ["two", "four"])) == 1
deck.tags.setGroupForTags(["three"], [], 3)
assert deck.db.scalar("select count() from cards where gid = 3") == 3
deck.tags.setGroupForTags(["one"], [], 2)
assert deck.db.scalar("select count() from cards where gid = 2") == 2
deck.tags.setDeckForTags(["three"], [], 3)
assert deck.db.scalar("select count() from cards where did = 3") == 3
deck.tags.setDeckForTags(["one"], [], 2)
assert deck.db.scalar("select count() from cards where did = 2") == 2
def test_addDelTags():
deck = getEmptyDeck()

96
tests/test_decks.py Normal file
View file

@ -0,0 +1,96 @@
# coding: utf-8
from tests.shared import assertException, getEmptyDeck, testDir
def test_basic():
deck = getEmptyDeck()
# we start with a standard deck
assert len(deck.decks.decks) == 1
# it should have an id of 1
assert deck.decks.name(1)
# create a new deck
parentId = deck.decks.id("new deck")
assert parentId
assert len(deck.decks.decks) == 2
# should get the same id
assert deck.decks.id("new deck") == parentId
# we start with the default deck selected
assert deck.decks.selected() == 1
assert deck.decks.active() == [1]
assert deck.decks.top()['id'] == 1
# we can select a different deck
deck.decks.select(parentId)
assert deck.decks.selected() == parentId
assert deck.decks.active() == [parentId]
assert deck.decks.top()['id'] == parentId
# let's create a child
childId = deck.decks.id("new deck::child")
# it should have been added to the active list
assert deck.decks.selected() == parentId
assert deck.decks.active() == [parentId, childId]
assert deck.decks.top()['id'] == parentId
# we can select the child individually too
deck.decks.select(childId)
assert deck.decks.selected() == childId
assert deck.decks.active() == [childId]
assert deck.decks.top()['id'] == parentId
def test_remove():
deck = getEmptyDeck()
# can't remove the default deck
assertException(AssertionError, lambda: deck.decks.rem(1))
# create a new deck, and add a note/card to it
g1 = deck.decks.id("g1")
f = deck.newNote()
f['Front'] = u"1"
f.did = g1
deck.addNote(f)
c = f.cards()[0]
assert c.did == g1
# by default deleting the deck leaves the cards with an invalid did
assert deck.cardCount() == 1
deck.decks.rem(g1)
assert deck.cardCount() == 1
c.load()
assert c.did == g1
# but if we try to get it, we get the default
assert deck.decks.name(c.did) == "Default"
# let's create another deck and explicitly set the card to it
g2 = deck.decks.id("g2")
c.did = g2; c.flush()
# this time we'll delete the card/note too
deck.decks.rem(g2, cardsToo=True)
assert deck.cardCount() == 0
assert deck.noteCount() == 0
def test_rename():
d = getEmptyDeck()
id = d.decks.id("hello::world")
# should be able to rename into a completely different branch, creating
# parents as necessary
d.decks.rename(d.decks.get(id), "foo::bar")
assert "foo" in d.decks.allNames()
assert "foo::bar" in d.decks.allNames()
assert "hello::world" not in d.decks.allNames()
# create another deck
id = d.decks.id("tmp")
# we can't rename it if it conflicts
assertException(
Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
# when renaming, the children should be renamed too
d.decks.id("one::two::three")
id = d.decks.id("one")
d.decks.rename(d.decks.get(id), "yo")
for n in "yo", "yo::two", "yo::two::three":
assert n in d.decks.allNames()
def test_topRename():
d = getEmptyDeck()
id = d.decks.id("hello::world")
# when moving to or from top level, properties should be updated
assert 'newSpread' in d.decks.get(d.decks.id("hello"))
assert 'newSpread' not in d.decks.get(d.decks.id("hello::world"))
d.decks.rename(d.decks.get(d.decks.id("hello")), "foo::bar")
assert 'newSpread' not in d.decks.get(d.decks.id("foo::bar"))
d.decks.rename(d.decks.get(d.decks.id("foo::bar")), "hello")
assert 'newSpread' in d.decks.get(d.decks.id("hello"))

View file

@ -104,10 +104,10 @@ def test_findCards():
assert len(deck.findCards("model:basic")) == 5
assert len(deck.findCards("-model:basic")) == 0
assert len(deck.findCards("-model:foo")) == 5
# group
assert len(deck.findCards("group:default")) == 5
assert len(deck.findCards("-group:default")) == 0
assert len(deck.findCards("-group:foo")) == 5
# deck
assert len(deck.findCards("deck:default")) == 5
assert len(deck.findCards("-deck:default")) == 0
assert len(deck.findCards("-deck:foo")) == 5
# full search
f = deck.newNote()
f['Front'] = u'hello<b>world</b>'

View file

@ -1,96 +0,0 @@
# coding: utf-8
from tests.shared import assertException, getEmptyDeck, testDir
def test_basic():
deck = getEmptyDeck()
# we start with a standard group
assert len(deck.groups.groups) == 1
# it should have an id of 1
assert deck.groups.name(1)
# create a new group
parentId = deck.groups.id("new group")
assert parentId
assert len(deck.groups.groups) == 2
# should get the same id
assert deck.groups.id("new group") == parentId
# we start with the default group selected
assert deck.groups.selected() == 1
assert deck.groups.active() == [1]
assert deck.groups.top()['id'] == 1
# we can select a different group
deck.groups.select(parentId)
assert deck.groups.selected() == parentId
assert deck.groups.active() == [parentId]
assert deck.groups.top()['id'] == parentId
# let's create a child
childId = deck.groups.id("new group::child")
# it should have been added to the active list
assert deck.groups.selected() == parentId
assert deck.groups.active() == [parentId, childId]
assert deck.groups.top()['id'] == parentId
# we can select the child individually too
deck.groups.select(childId)
assert deck.groups.selected() == childId
assert deck.groups.active() == [childId]
assert deck.groups.top()['id'] == parentId
def test_remove():
deck = getEmptyDeck()
# can't remove the default group
assertException(AssertionError, lambda: deck.groups.rem(1))
# create a new group, and add a note/card to it
g1 = deck.groups.id("g1")
f = deck.newNote()
f['Front'] = u"1"
f.gid = g1
deck.addNote(f)
c = f.cards()[0]
assert c.gid == g1
# by default deleting the group leaves the cards with an invalid gid
assert deck.cardCount() == 1
deck.groups.rem(g1)
assert deck.cardCount() == 1
c.load()
assert c.gid == g1
# but if we try to get it, we get the default
assert deck.groups.name(c.gid) == "Default"
# let's create another group and explicitly set the card to it
g2 = deck.groups.id("g2")
c.gid = g2; c.flush()
# this time we'll delete the card/note too
deck.groups.rem(g2, cardsToo=True)
assert deck.cardCount() == 0
assert deck.noteCount() == 0
def test_rename():
d = getEmptyDeck()
id = d.groups.id("hello::world")
# should be able to rename into a completely different branch, creating
# parents as necessary
d.groups.rename(d.groups.get(id), "foo::bar")
assert "foo" in d.groups.allNames()
assert "foo::bar" in d.groups.allNames()
assert "hello::world" not in d.groups.allNames()
# create another group
id = d.groups.id("tmp")
# we can't rename it if it conflicts
assertException(
Exception, lambda: d.groups.rename(d.groups.get(id), "foo"))
# when renaming, the children should be renamed too
d.groups.id("one::two::three")
id = d.groups.id("one")
d.groups.rename(d.groups.get(id), "yo")
for n in "yo", "yo::two", "yo::two::three":
assert n in d.groups.allNames()
def test_topRename():
d = getEmptyDeck()
id = d.groups.id("hello::world")
# when moving to or from top level, properties should be updated
assert 'newSpread' in d.groups.get(d.groups.id("hello"))
assert 'newSpread' not in d.groups.get(d.groups.id("hello::world"))
d.groups.rename(d.groups.get(d.groups.id("hello")), "foo::bar")
assert 'newSpread' not in d.groups.get(d.groups.id("foo::bar"))
d.groups.rename(d.groups.get(d.groups.id("foo::bar")), "hello")
assert 'newSpread' in d.groups.get(d.groups.id("hello"))

View file

@ -54,29 +54,29 @@ def test_new():
def test_newLimits():
d = getEmptyDeck()
# add some notes
g2 = d.groups.id("Default::foo")
g2 = d.decks.id("Default::foo")
for i in range(30):
f = d.newNote()
f['Front'] = str(i)
if i > 4:
f.gid = g2
f.did = g2
d.addNote(f)
# give the child group a different configuration
c2 = d.groups.confId("new conf")
d.groups.setConf(d.groups.get(g2), c2)
# give the child deck a different configuration
c2 = d.decks.confId("new conf")
d.decks.setConf(d.decks.get(g2), c2)
d.reset()
# both confs have defaulted to a limit of 20
assert d.sched.newCount == 20
# first card we get comes from parent
c = d.sched.getCard()
assert c.gid == 1
assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.groups.conf(1)
conf1 = d.decks.conf(1)
conf1['new']['perDay'] = 10
d.reset()
assert d.sched.newCount == 10
# if we limit child to 4, we should get 9
conf2 = d.groups.conf(g2)
conf2 = d.decks.conf(g2)
conf2['new']['perDay'] = 4
d.reset()
assert d.sched.newCount == 9
@ -98,7 +98,7 @@ def test_newOrder():
# add first half
d.addNote(f)
# generate second half
d.db.execute("update cards set gid = random()")
d.db.execute("update cards set did = random()")
d.conf['newPerDay'] = 100
d.reset()
# cards should be sorted by id
@ -415,8 +415,8 @@ def test_cram():
c.startTimer()
c.flush()
cardcopy = copy.copy(c)
d.conf['groups'] = [1]
d.cramGroups()
d.conf['decks'] = [1]
d.cramDecks()
# first, test with initial intervals preserved
conf = d.sched._lrnConf(c)
conf['reset'] = False
@ -452,7 +452,7 @@ def test_cram():
# now try again with ivl rescheduling
c = copy.copy(cardcopy)
c.flush()
d.cramGroups()
d.cramDecks()
conf = d.sched._lrnConf(c)
conf['reset'] = False
conf['resched'] = True
@ -467,7 +467,7 @@ def test_cram():
# try with ivl reset
c = copy.copy(cardcopy)
c.flush()
d.cramGroups()
d.cramDecks()
conf = d.sched._lrnConf(c)
conf['reset'] = True
conf['resched'] = True
@ -477,8 +477,8 @@ def test_cram():
assert c.ivl == 1
assert c.due == d.sched.today + 1
# users should be able to cram entire deck too
d.conf['groups'] = []
d.cramGroups()
d.conf['decks'] = []
d.cramDecks()
assert d.sched.cardCounts()[0] > 0
def test_cramLimits():
@ -493,30 +493,30 @@ def test_cramLimits():
c.due = d.sched.today + 1 + i
c.flush()
# the default cram should return all three
d.conf['groups'] = [1]
d.cramGroups()
d.conf['decks'] = [1]
d.cramDecks()
assert d.sched.cardCounts()[0] == 3
# if we start from the day after tomorrow, it should be 2
d.cramGroups(min=1)
d.cramDecks(min=1)
assert d.sched.cardCounts()[0] == 2
# or after 2 days
d.cramGroups(min=2)
d.cramDecks(min=2)
assert d.sched.cardCounts()[0] == 1
# we may get nothing
d.cramGroups(min=3)
d.cramDecks(min=3)
assert d.sched.cardCounts()[0] == 0
# tomorrow(0) + dayAfter(1) = 2
d.cramGroups(max=1)
d.cramDecks(max=1)
assert d.sched.cardCounts()[0] == 2
# if max is tomorrow, we get only one
d.cramGroups(max=0)
d.cramDecks(max=0)
assert d.sched.cardCounts()[0] == 1
# both should work
d.cramGroups(min=0, max=0)
d.cramDecks(min=0, max=0)
assert d.sched.cardCounts()[0] == 1
d.cramGroups(min=1, max=1)
d.cramDecks(min=1, max=1)
assert d.sched.cardCounts()[0] == 1
d.cramGroups(min=0, max=1)
d.cramDecks(min=0, max=1)
assert d.sched.cardCounts()[0] == 2
def test_adjIvl():
@ -614,28 +614,28 @@ def test_ordcycle():
def test_cardcounts():
d = getEmptyDeck()
# add a second group
grp = d.groups.id("Default::new group")
# add a second deck
grp = d.decks.id("Default::new deck")
# for each card type
for type in range(3):
# and each of the groups
for gid in (1,grp):
# and each of the decks
for did in (1,grp):
# create a new note
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
c = f.cards()[0]
# set type/gid
# set type/did
c.type = type
c.queue = type
c.gid = gid
c.did = did
c.due = 0
c.flush()
d.reset()
# with the default settings, there's no count limit
assert d.sched.cardCounts() == (2,2,2)
# check limit to one group
d.groups.select(grp)
# check limit to one deck
d.decks.select(grp)
d.reset()
assert d.sched.cardCounts() == (1,1,1)
@ -753,42 +753,42 @@ def test_collapse():
d.sched.answerCard(c, 3)
assert not d.sched.getCard()
def test_groupCounts():
def test_deckCounts():
d = getEmptyDeck()
# add a note with default group
# add a note with default deck
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = u"two"
default1 = f.gid = d.groups.id("Default::1")
default1 = f.did = d.decks.id("Default::1")
d.addNote(f)
# make it a review card
c = f.cards()[0]
c.queue = 2
c.due = 0
c.flush()
# add one more with a new group
# add one more with a new deck
f = d.newNote()
f['Front'] = u"two"
foobar = f.gid = d.groups.id("foo::bar")
foobar = f.did = d.decks.id("foo::bar")
d.addNote(f)
# and one that's a sibling
f = d.newNote()
f['Front'] = u"three"
foobaz = f.gid = d.groups.id("foo::baz")
foobaz = f.did = d.decks.id("foo::baz")
d.addNote(f)
d.reset()
assert len(d.groups.groups) == 5
cnts = d.sched.groupCounts()
assert len(d.decks.decks) == 5
cnts = d.sched.deckCounts()
cnts.sort()
assert cnts[0] == ["Default", 1, 0, 1]
assert cnts[1] == ["Default::1", default1, 1, 0]
assert cnts[2] == ["foo", d.groups.id("foo"), 0, 0]
assert cnts[2] == ["foo", d.decks.id("foo"), 0, 0]
assert cnts[3] == ["foo::bar", foobar, 0, 1]
assert cnts[4] == ["foo::baz", foobaz, 0, 1]
tree = d.sched.groupCountTree()
tree = d.sched.deckCountTree()
assert tree[0][0] == "Default"
# sum of child and parent
assert tree[0][1] == 1
@ -799,35 +799,35 @@ def test_groupCounts():
assert tree[0][4][0][1] == default1
assert tree[0][4][0][2] == 1
assert tree[0][4][0][3] == 0
# code should not fail if a card has an invalid group
c.gid = 12345; c.flush()
d.sched.groupCounts()
d.sched.groupCountTree()
# code should not fail if a card has an invalid deck
c.did = 12345; c.flush()
d.sched.deckCounts()
d.sched.deckCountTree()
def test_groupTree():
def test_deckTree():
d = getEmptyDeck()
d.groups.id("new::b::c")
d.groups.id("new2")
d.decks.id("new::b::c")
d.decks.id("new2")
# new should not appear twice in tree
names = [x[0] for x in d.sched.groupCountTree()]
names = [x[0] for x in d.sched.deckCountTree()]
names.remove("new")
assert "new" not in names
def test_groupFlow():
def test_deckFlow():
d = getEmptyDeck()
# add a note with default group
# add a note with default deck
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = u"two"
default1 = f.gid = d.groups.id("Default::2")
default1 = f.did = d.decks.id("Default::2")
d.addNote(f)
# and another that's higher up
f = d.newNote()
f['Front'] = u"three"
default1 = f.gid = d.groups.id("Default::1")
default1 = f.did = d.decks.id("Default::1")
d.addNote(f)
# should get top level one first, then ::1, then ::2
d.reset()
@ -839,7 +839,7 @@ def test_groupFlow():
def test_reorder():
d = getEmptyDeck()
# add a note with default group
# add a note with default deck
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -917,7 +917,7 @@ def test_revlim():
for i in range(5):
d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.repCounts()[2] == 15
t = d.groups.top()
t = d.decks.top()
t['revLim'] = 10
d.reset()
assert d.sched.repCounts()[2] == 5

View file

@ -65,9 +65,9 @@ def test_sync():
for t in ("revlog", "notes", "cards", "nsums"):
assert d.db.scalar("select count() from %s" % t) == num
assert len(d.models.all()) == num*2
# the default group and config have an id of 1, so always 1
assert len(d.groups.all()) == 1
assert len(d.groups.gconf) == 1
# the default deck and config have an id of 1, so always 1
assert len(d.decks.all()) == 1
assert len(d.decks.dconf) == 1
assert len(d.tags.all()) == num
check(1)
origUsn = deck1.usn()
@ -173,35 +173,35 @@ def test_tags():
assert deck1.tags.all() == deck2.tags.all()
@nose.with_setup(setup_modified)
def test_groups():
def test_decks():
test_sync()
assert len(deck1.groups.all()) == 1
assert len(deck1.groups.all()) == len(deck2.groups.all())
deck1.groups.id("new")
assert len(deck1.groups.all()) != len(deck2.groups.all())
assert len(deck1.decks.all()) == 1
assert len(deck1.decks.all()) == len(deck2.decks.all())
deck1.decks.id("new")
assert len(deck1.decks.all()) != len(deck2.decks.all())
time.sleep(0.1)
deck2.groups.id("new2")
deck2.decks.id("new2")
deck1.save()
deck2.save()
assert client.sync() == "success"
assert deck1.tags.all() == deck2.tags.all()
assert len(deck1.groups.all()) == len(deck2.groups.all())
assert len(deck1.groups.all()) == 3
assert deck1.groups.conf(1)['maxTaken'] == 60
deck2.groups.conf(1)['maxTaken'] = 30
deck2.groups.save(deck2.groups.conf(1))
assert len(deck1.decks.all()) == len(deck2.decks.all())
assert len(deck1.decks.all()) == 3
assert deck1.decks.conf(1)['maxTaken'] == 60
deck2.decks.conf(1)['maxTaken'] = 30
deck2.decks.save(deck2.decks.conf(1))
deck2.save()
assert client.sync() == "success"
assert deck1.groups.conf(1)['maxTaken'] == 30
assert deck1.decks.conf(1)['maxTaken'] == 30
@nose.with_setup(setup_modified)
def test_conf():
test_sync()
assert deck2.conf['topGroup'] == 1
deck1.conf['topGroup'] = 2
assert deck2.conf['topDeck'] == 1
deck1.conf['topDeck'] = 2
deck1.save()
assert client.sync() == "success"
assert deck2.conf['topGroup'] == 2
assert deck2.conf['topDeck'] == 2
@nose.with_setup(setup_modified)
def test_threeway():