Anki/anki/decks.py
Damien Elmes 8c9c3489e5 fetch reviews in subdecks incrementally like new cards
- allows separate review order for different decks
- makes new card and rev card handling consistent
- for users who find it confusing to have cards from different decks mixed in
  and thus click on each deck in turn, they can now just select the parent
  deck and have it work as expected
- for users who want their cards mixed together randomly, they can keep the
  cards in a single deck
2011-12-09 04:02:43 +09:00

348 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import simplejson, copy
from anki.utils import intTime, ids2str
from anki.consts import *
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 decks
# - make sure all children have parents (create as necessary)
# - when renaming a deck, top level properties should be added or removed as
# appropriate
# notes:
# - 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
defaultDeck = {
'newToday': [0, 0], # currentDay, count
'revToday': [0, 0],
'lrnToday': [0, 0],
'timeToday': [0, 0], # time in ms
'conf': 1,
'usn': 0,
}
# configuration only available to top level decks
defaultTopConf = {
'newSpread': NEW_CARDS_DISTRIBUTE,
'collapseTime': 1200,
'repLim': 0,
'timeLim': 600,
'curModel': None,
}
# configuration available to all decks
defaultConf = {
'name': _("Default"),
'new': {
'delays': [1, 10],
'ints': [1, 4],
'initialFactor': 2500,
'order': NEW_TODAY_ORD,
'perDay': 20,
},
'lapse': {
'delays': [1, 10],
'mult': 0,
'minInt': 1,
'relearn': True,
'leechFails': 8,
# type 0=suspend, 1=tagonly
'leechAction': 0,
},
'cram': {
'delays': [1, 5, 10],
'resched': True,
'reset': True,
'mult': 0,
'minInt': 1,
},
'rev': {
'perDay': 100,
'ease4': 1.3,
'fuzz': 0.05,
'minSpace': 1,
'fi': [0.1, 0.1],
'order': REV_CARDS_RANDOM,
},
'maxTaken': 60,
'mod': 0,
'usn': 0,
}
class DeckManager(object):
# Registry save/load
#############################################################
def __init__(self, col):
self.col = col
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 deck or a deck configuration."
if g:
g['mod'] = intTime()
g['usn'] = self.col.usn()
self.changed = True
def flush(self):
if self.changed:
self.col.db.execute("update col set decks=?, dconf=?",
simplejson.dumps(self.decks),
simplejson.dumps(self.dconf))
self.changed = False
# Deck save/load
#############################################################
def id(self, name, create=True):
"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 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 defaultDeck.items():
g[k] = v
g['name'] = name
while 1:
id = intTime(1000)
if str(id) not in self.decks:
break
g['id'] = id
self.decks[str(id)] = g
self.save(g)
self.maybeAddToActive()
return int(id)
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(did):
self.rem(id, cardsToo)
# delete cards too?
if cardsToo:
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 deck names."
return [x['name'] for x in self.decks.values()]
def all(self):
"A list of all decks."
return self.decks.values()
def count(self):
return len(self.decks)
def get(self, did, default=True):
id = str(did)
if id in self.decks:
return self.decks[id]
elif default:
return self.decks['1']
def update(self, 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 deck prefix to NAME if not exists. Updates children."
# make sure target node doesn't already exist
if newName in self.allNames():
raise Exception("Deck exists")
# rename children
for grp in self.all():
if grp['name'].startswith(g['name'] + "::"):
grp['name'] = grp['name'].replace(g['name']+ "::",
newName + "::")
self.save(grp)
# adjust top level conf
if "::" in newName and "::" not in g['name']:
for k in defaultTopConf.keys():
del g[k]
elif "::" not in newName and "::" in g['name']:
for k,v in defaultTopConf.items():
g[k] = v
# adjust name and save
g['name'] = newName
self.save(g)
# finally, ensure we have parents
self._ensureParents(newName)
def _ensureParents(self, name):
path = name.split("::")
s = ""
for p in path[:-1]:
if not s:
s += p
else:
s += "::" + p
self.id(s)
# Deck configurations
#############################################################
def allConf(self):
"A list of all deck config."
return self.dconf.values()
def conf(self, did):
return self.dconf[str(self.decks[str(did)]['conf'])]
def updateConf(self, g):
self.dconf[str(g['id'])] = g
self.save()
def confId(self, name):
"Create a new configuration and return id."
c = copy.deepcopy(defaultConf)
while 1:
id = intTime(1000)
if str(id) not in self.dconf:
break
c['id'] = id
c['name'] = name
self.dconf[str(id)] = c
self.save(c)
return id
def remConf(self, id):
"Remove a configuration and update all decks using it."
assert int(id) != 1
self.col.modSchema()
del self.dconf[str(id)]
for g in self.all():
if str(g['conf']) == str(id):
g['conf'] = 1
self.save(g)
def setConf(self, grp, id):
grp['conf'] = id
self.save(grp)
# Deck utils
#############################################################
def name(self, did):
return self.get(did)['name']
def setDeck(self, cids, did):
self.col.db.execute(
"update cards set did=?,usn=?,mod=? where id in "+
ids2str(cids), did, self.col.usn(), intTime())
def maybeAddToActive(self):
# reselect current deck, or default if current has disappeared
c = self.current()
self.select(c['id'])
def sendHome(self, cids):
self.col.db.execute("""
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(), did)
def cids(self, did):
return self.col.db.list("select id from cards where did=?", did)
# Deck selection
#############################################################
def top(self):
"The current top level deck as an object."
g = self.get(self.col.conf['topDeck'])
return g
def topIds(self):
"All dids from top level."
t = self.top()
return [t['id']] + [a[1] for a in self.children(t['id'])]
def active(self):
"The currrently active dids."
return self.col.conf['activeDecks']
def selected(self):
"The currently selected did."
return self.col.conf['curDeck']
def current(self):
return self.get(self.selected())
def select(self, did):
"Select a new branch."
# 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['activeDecks'] = [did] + [a[1] for a in actv]
self.changed = True
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, 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 did for NAME."
path = name.split("::")
return self.id(path[0])
# Sync handling
##########################################################################
def beforeUpload(self):
for d in self.all():
d['usn'] = 0
for c in self.allConf():
c['usn'] = 0
self.save()