mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
347 lines
10 KiB
Python
347 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 = {
|
|
'revLim': 100,
|
|
'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': {
|
|
'ease4': 1.3,
|
|
'fuzz': 0.05,
|
|
'minSpace': 1,
|
|
'fi': [0.1, 0.1],
|
|
},
|
|
'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()
|