mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00

- 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
348 lines
10 KiB
Python
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()
|