move models into the deck table

Like the previous change, models have been moved from a separate DB table to
an entry in the deck. We need them for many operations including reviewing,
and it's easier to keep them in memory than half on disk with a cache that
gets cleared every time we .reset(). This means they are easily serialized as
well - previously they were part Python and part JSON, which made access
confusing.

Because the data is all pulled from JSON now, the instance methods have been
moved to the model registry. Eg:
  model.addField(...) -> deck.models.addField(model, ...).

- IDs are now timestamped as with groups et al.

- The data field for plugins was also removed. Config info can be added to
  deck.conf; larger data should be stored externally.

- Upgrading needs to be updated for the new model structure.

- HexifyID() now accepts strings as well, as our IDs get converted to strings
  in the serialization process.
This commit is contained in:
Damien Elmes 2011-08-27 22:27:09 +09:00
parent 7afe6a9a7d
commit d3a3edb707
19 changed files with 388 additions and 390 deletions

View file

@ -105,9 +105,9 @@ lapses=?, grade=?, cycles=?, edue=? where id = ?""",
def _getQA(self, reload=False): def _getQA(self, reload=False):
if not self._qa or reload: if not self._qa or reload:
f = self.fact(); m = self.model() f = self.fact(); 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(self.model(), data) self._qa = self.deck._renderQA(data)
return self._qa return self._qa
def _withClass(self, txt, extra): def _withClass(self, txt, extra):
@ -117,7 +117,7 @@ lapses=?, grade=?, cycles=?, edue=? where id = ?""",
"Fetch the model and fact." "Fetch the model and fact."
if not self._rd or reload: if not self._rd or reload:
f = self.deck.getFact(self.fid) f = self.deck.getFact(self.fid)
m = self.deck.getModel(f.mid) m = self.deck.models.get(f.mid)
self._rd = [f, m] self._rd = [f, m]
return self._rd return self._rd
@ -128,10 +128,10 @@ lapses=?, grade=?, cycles=?, edue=? where id = ?""",
return self._reviewData()[1] return self._reviewData()[1]
def template(self): def template(self):
return self._reviewData()[1].templates[self.ord] return self._reviewData()[1]['tmpls'][self.ord]
def cssClass(self): def cssClass(self):
return "cm%s-%s" % (hexifyID(self.model().id), return "cm%s-%s" % (hexifyID(self.model()['id']),
hexifyID(self.template()['ord'])) hexifyID(self.template()['ord']))
def startTimer(self): def startTimer(self):

View file

@ -9,12 +9,13 @@ from anki.utils import parseTags, ids2str, hexifyID, \
splitFields splitFields
from anki.hooks import runHook, runFilter from anki.hooks import runHook, runFilter
from anki.sched import Scheduler from anki.sched import Scheduler
from anki.models import ModelRegistry
from anki.media import MediaRegistry from anki.media import MediaRegistry
from anki.consts import * from anki.consts import *
from anki.errors import AnkiError from anki.errors import AnkiError
import anki.latex # sets up hook import anki.latex # sets up hook
import anki.cards, anki.facts, anki.models, anki.template, anki.cram, \ import anki.cards, anki.facts, anki.template, anki.cram, \
anki.groups, anki.find anki.groups, anki.find
# Settings related to queue building. These may be loaded without the rest of # Settings related to queue building. These may be loaded without the rest of
@ -51,6 +52,8 @@ class _Deck(object):
self.path = db._path self.path = db._path
self._lastSave = time.time() self._lastSave = time.time()
self.clearUndo() self.clearUndo()
self.media = MediaRegistry(self)
self.models = ModelRegistry(self)
self.load() self.load()
if not self.crt: if not self.crt:
d = datetime.datetime.today() d = datetime.datetime.today()
@ -65,7 +68,6 @@ class _Deck(object):
self.lastSessionStart = 0 self.lastSessionStart = 0
self._stdSched = Scheduler(self) self._stdSched = Scheduler(self)
self.sched = self._stdSched self.sched = self._stdSched
self.media = MediaRegistry(self)
# check for improper shutdown # check for improper shutdown
self.cleanup() self.cleanup()
@ -85,16 +87,16 @@ class _Deck(object):
self.lastSync, self.lastSync,
self.qconf, self.qconf,
self.conf, self.conf,
models,
self.groups, self.groups,
self.gconf, self.gconf) = self.db.first("""
self.data) = self.db.first("""
select crt, mod, scm, dty, syncName, lastSync, select crt, mod, scm, dty, syncName, lastSync,
qconf, conf, groups, gconf, data from deck""") qconf, conf, models, groups, gconf from deck""")
self.qconf = simplejson.loads(self.qconf) self.qconf = simplejson.loads(self.qconf)
self.conf = simplejson.loads(self.conf) self.conf = simplejson.loads(self.conf)
self.groups = simplejson.loads(self.groups) self.groups = simplejson.loads(self.groups)
self.gconf = simplejson.loads(self.gconf) self.gconf = simplejson.loads(self.gconf)
self.data = simplejson.loads(self.data) self.models.load(models)
def flush(self, mod=None): def flush(self, mod=None):
"Flush state to DB, updating mod time." "Flush state to DB, updating mod time."
@ -102,11 +104,14 @@ qconf, conf, groups, gconf, data from deck""")
self.db.execute( self.db.execute(
"""update deck set """update deck set
crt=?, mod=?, scm=?, dty=?, syncName=?, lastSync=?, crt=?, mod=?, scm=?, dty=?, syncName=?, lastSync=?,
qconf=?, conf=?, data=?""", qconf=?, conf=?, groups=?, gconf=?""",
self.crt, self.mod, self.scm, self.dty, self.crt, self.mod, self.scm, self.dty,
self.syncName, self.lastSync, self.syncName, self.lastSync,
simplejson.dumps(self.qconf), simplejson.dumps(self.qconf),
simplejson.dumps(self.conf), simplejson.dumps(self.data)) simplejson.dumps(self.conf),
simplejson.dumps(self.groups),
simplejson.dumps(self.gconf))
self.models.flush()
def save(self, name=None, mod=None): def save(self, name=None, mod=None):
"Flush, commit DB, and take out another write lock." "Flush, commit DB, and take out another write lock."
@ -187,15 +192,6 @@ qconf=?, conf=?, data=?""",
def getFact(self, id): def getFact(self, id):
return anki.facts.Fact(self, id=id) return anki.facts.Fact(self, id=id)
def getModel(self, mid, cache=True):
"Memoizes; call .reset() to reset cache."
if cache and mid in self.modelCache:
return self.modelCache[mid]
m = anki.models.Model(self, mid)
if cache:
self.modelCache[mid] = m
return m
# Utils # Utils
########################################################################## ##########################################################################
@ -227,7 +223,7 @@ qconf=?, conf=?, data=?""",
def newFact(self): def newFact(self):
"Return a new fact with the current model." "Return a new fact with the current model."
return anki.facts.Fact(self, self.currentModel()) return anki.facts.Fact(self, self.models.current())
def addFact(self, fact): def addFact(self, fact):
"Add a fact to the deck. Return number of new cards." "Add a fact to the deck. Return number of new cards."
@ -273,14 +269,14 @@ qconf=?, conf=?, data=?""",
"Return (active), non-empty templates." "Return (active), non-empty templates."
ok = [] ok = []
model = fact.model() model = fact.model()
for template in model.templates: for template in model['tmpls']:
if template['actv'] or not checkActive: if template['actv'] or not checkActive:
# [cid, fid, mid, gid, ord, tags, flds] # [cid, fid, mid, gid, ord, tags, flds]
data = [1, 1, model.id, 1, template['ord'], data = [1, 1, model['id'], 1, template['ord'],
"", fact.joinedFields()] "", fact.joinedFields()]
now = self._renderQA(model, data) now = self._renderQA(data)
data[6] = "\x1f".join([""]*len(fact.fields)) data[6] = "\x1f".join([""]*len(fact.fields))
empty = self._renderQA(model, data) empty = self._renderQA(data)
if now['q'] == empty['q']: if now['q'] == empty['q']:
continue continue
if not template['emptyAns']: if not template['emptyAns']:
@ -321,7 +317,7 @@ qconf=?, conf=?, data=?""",
elif type == 1: elif type == 1:
cms = [c.template() for c in fact.cards()] cms = [c.template() for c in fact.cards()]
else: else:
cms = fact.model().templates cms = fact.model()['tmpls']
if not cms: if not cms:
return [] return []
cards = [] cards = []
@ -365,45 +361,6 @@ select id from facts where id in %s and id not in (select fid from cards)""" %
ids2str(fids)) ids2str(fids))
self._delFacts(fids) self._delFacts(fids)
# Models
##########################################################################
def currentModel(self):
return self.getModel(self.conf['currentModelId'])
def models(self):
"Return a dict of mid -> model."
mods = {}
for m in [self.getModel(id) for id in self.db.list(
"select id from models")]:
mods[m.id] = m
return mods
def addModel(self, model):
self.modSchema()
model.flush()
self.conf['currentModelId'] = model.id
def delModel(self, mid):
"Delete MODEL, and all its cards/facts."
self.modSchema()
# delete facts/cards
self.delCards(self.db.list("""
select id from cards where fid in (select id from facts where mid = ?)""",
mid))
# then the model
self.db.execute("delete from models where id = ?", mid)
# GUI should ensure last model is not deleted
if self.conf['currentModelId'] == mid:
self.conf['currentModelId'] = self.db.scalar(
"select id from models limit 1")
def allCSS(self):
return "\n".join(self.db.list("select css from models"))
def modelId(self, name):
return self.db.scalar("select id from models where name = ?", name)
# Field checksums and sorting fields # Field checksums and sorting fields
########################################################################## ##########################################################################
@ -414,17 +371,16 @@ select id from cards where fid in (select id from facts where mid = ?)""",
def updateFieldCache(self, fids, csum=True): def updateFieldCache(self, fids, csum=True):
"Update field checksums and sort cache, after find&replace, etc." "Update field checksums and sort cache, after find&replace, etc."
sfids = ids2str(fids) sfids = ids2str(fids)
mods = self.models()
r = [] r = []
r2 = [] r2 = []
for (fid, mid, flds) in self._fieldData(sfids): for (fid, mid, flds) in self._fieldData(sfids):
fields = splitFields(flds) fields = splitFields(flds)
model = mods[mid] model = self.models.get(mid)
if csum: if csum:
for f in model.fields: for f in model['flds']:
if f['uniq'] and fields[f['ord']]: if f['uniq'] and fields[f['ord']]:
r.append((fid, mid, fieldChecksum(fields[f['ord']]))) r.append((fid, mid, fieldChecksum(fields[f['ord']])))
r2.append((stripHTML(fields[model.sortIdx()]), fid)) r2.append((stripHTML(fields[self.models.sortIdx(model)]), fid))
if csum: if csum:
self.db.execute("delete from fsums where fid in "+sfids) self.db.execute("delete from fsums where fid in "+sfids)
self.db.executemany("insert into fsums values (?,?,?)", r) self.db.executemany("insert into fsums values (?,?,?)", r)
@ -445,17 +401,17 @@ select id from cards where fid in (select id from facts where mid = ?)""",
where = "" where = ""
else: else:
raise Exception() raise Exception()
mods = self.models() return [self._renderQA(row)
return [self._renderQA(mods[row[2]], row)
for row in self._qaData(where)] for row in self._qaData(where)]
def _renderQA(self, model, data): def _renderQA(self, data):
"Returns hash of id, question, answer." "Returns hash of id, question, answer."
# data is [cid, fid, mid, gid, ord, tags, flds] # data is [cid, fid, mid, gid, ord, tags, flds]
# unpack fields and create dict # unpack fields and create dict
flist = splitFields(data[6]) flist = splitFields(data[6])
fields = {} fields = {}
for (name, (idx, conf)) in model.fieldMap().items(): model = self.models.get(data[2])
for (name, (idx, conf)) in self.models.fieldMap(model).items():
fields[name] = flist[idx] fields[name] = flist[idx]
if fields[name]: if fields[name]:
fields[name] = '<span class="fm%s-%s">%s</span>' % ( fields[name] = '<span class="fm%s-%s">%s</span>' % (
@ -463,9 +419,9 @@ select id from cards where fid in (select id from facts where mid = ?)""",
else: else:
fields[name] = "" fields[name] = ""
fields['Tags'] = data[5] fields['Tags'] = data[5]
fields['Model'] = model.name fields['Model'] = model['name']
fields['Group'] = self.groupName(data[3]) fields['Group'] = self.groupName(data[3])
template = model.templates[data[4]] template = model['tmpls'][data[4]]
fields['Template'] = template['name'] fields['Template'] = template['name']
# render q & a # render q & a
d = dict(id=data[0]) d = dict(id=data[0])
@ -473,7 +429,7 @@ select id from cards where fid in (select id from facts where mid = ?)""",
if type == "q": if type == "q":
format = format.replace("cloze:", "cq:") format = format.replace("cloze:", "cq:")
else: else:
if model.conf['clozectx']: if model['clozectx']:
name = "cactx:" name = "cactx:"
else: else:
name = "ca:" name = "ca:"
@ -770,8 +726,8 @@ select id from facts where id not in (select distinct fid from cards)""")
self.db.execute("delete from tags") self.db.execute("delete from tags")
self.updateFactTags() self.updateFactTags()
# field cache # field cache
for m in self.models().values(): for m in self.models.all():
self.updateFieldCache(m.fids()) self.updateFieldCache(self.models.fids(m['id']))
# and finally, optimize # and finally, optimize
self.optimize() self.optimize()
newSize = os.stat(self.path)[stat.ST_SIZE] newSize = os.stat(self.path)[stat.ST_SIZE]

View file

@ -19,12 +19,12 @@ class Fact(object):
else: else:
self.id = timestampID(deck.db, "facts") self.id = timestampID(deck.db, "facts")
self._model = model self._model = model
self.gid = model.conf['gid'] self.gid = model['gid']
self.mid = model.id self.mid = model['id']
self.tags = [] self.tags = []
self.fields = [""] * len(self._model.fields) self.fields = [""] * len(self._model['flds'])
self.data = "" self.data = ""
self._fmap = self._model.fieldMap() self._fmap = self.deck.models.fieldMap(self._model)
def load(self): def load(self):
(self.mid, (self.mid,
@ -36,12 +36,12 @@ class Fact(object):
select mid, gid, mod, tags, flds, data from facts where id = ?""", self.id) select mid, gid, mod, tags, flds, data from facts where id = ?""", self.id)
self.fields = splitFields(self.fields) self.fields = splitFields(self.fields)
self.tags = parseTags(self.tags) self.tags = parseTags(self.tags)
self._model = self.deck.getModel(self.mid) self._model = self.deck.models.get(self.mid)
self._fmap = self._model.fieldMap() self._fmap = self.deck.models.fieldMap(self._model)
def flush(self): def flush(self):
self.mod = intTime() self.mod = intTime()
sfld = stripHTML(self.fields[self._model.sortIdx()]) sfld = stripHTML(self.fields[self.deck.models.sortIdx(self._model)])
tags = self.stringTags() tags = self.stringTags()
res = self.deck.db.execute(""" res = self.deck.db.execute("""
insert or replace into facts values (?, ?, ?, ?, ?, ?, ?, ?)""", insert or replace into facts values (?, ?, ?, ?, ?, ?, ?, ?)""",

View file

@ -20,8 +20,8 @@ SEARCH_GROUP = 7
def fieldNames(deck, downcase=True): def fieldNames(deck, downcase=True):
fields = set() fields = set()
names = [] names = []
for m in deck.models().values(): for m in deck.models.all():
for f in m.fields: 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'])
fields.add(f['name'].lower()) fields.add(f['name'].lower())
@ -119,7 +119,7 @@ order by %s""" % (lim, sort)
elif type == SEARCH_FIELD: elif type == SEARCH_FIELD:
self._findField(token, isNeg) self._findField(token, isNeg)
elif type == SEARCH_MODEL: elif type == SEARCH_MODEL:
self._findModel(token, isNeg, c) self._findModel(token, isNeg)
elif type == SEARCH_GROUP: elif type == SEARCH_GROUP:
self._findGroup(token, isNeg) self._findGroup(token, isNeg)
else: else:
@ -182,12 +182,13 @@ order by %s""" % (lim, sort)
def _findFids(self, val): def _findFids(self, val):
self.lims['fact'].append("id in (%s)" % val) self.lims['fact'].append("id in (%s)" % val)
def _findModel(self, val, isNeg, c): def _findModel(self, val, isNeg):
extra = "not" if isNeg else "" extra = "not" if isNeg else ""
self.lims['fact'].append( ids = []
"mid %s in (select id from models where name like :_mod_%d)" % ( for m in self.deck.models.all():
extra, c)) if m['name'].lower() == val:
self.lims['args']['_mod_%d'%c] = val ids.append(m['id'])
self.lims['fact'].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 ""
@ -203,8 +204,8 @@ order by %s""" % (lim, sort)
except: except:
num = None num = None
lims = [] lims = []
for m in self.deck.models().values(): for m in self.deck.models.all():
for t in m.templates: 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:
self.lims['card'].append("ord %s %d" % (comp, num)) self.lims['card'].append("ord %s %d" % (comp, num))
@ -212,8 +213,8 @@ order by %s""" % (lim, sort)
# template name? # template name?
elif t['name'].lower() == val.lower(): elif t['name'].lower() == val.lower():
lims.append(( lims.append((
"(fid in (select id from facts where mid = %d) " "(fid in (select id from facts where mid = %s) "
"and ord %s %d)") % (m.id, comp, t['ord'])) "and ord %s %d)") % (m['id'], comp, t['ord']))
found = True found = True
if lims: if lims:
self.lims['card'].append("(" + " or ".join(lims) + ")") self.lims['card'].append("(" + " or ".join(lims) + ")")
@ -226,10 +227,10 @@ 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().values(): for m in self.deck.models.all():
for f in m.fields: 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'])
if not mods: if not mods:
# nothing has that field # nothing has that field
self.lims['valid'] = False self.lims['valid'] = False
@ -243,11 +244,11 @@ where mid in %s and flds like ? escape '\\'""" % (
ids2str(mods.keys())), ids2str(mods.keys())),
"%" if self.full else value): "%" if self.full else value):
flds = splitFields(flds) flds = splitFields(flds)
ord = mods[mid][1] ord = mods[str(mid)][1]
str = flds[ord] strg = flds[ord]
if self.full: if self.full:
str = stripHTML(str) strg = stripHTML(strg)
if re.search(regex, str): if re.search(regex, strg):
fids.append(id) fids.append(id)
extra = "not" if isNeg else "" extra = "not" if isNeg else ""
self.lims['fact'].append("id %s in %s" % (extra, ids2str(fids))) self.lims['fact'].append("id %s in %s" % (extra, ids2str(fids)))
@ -372,10 +373,10 @@ def findReplace(deck, fids, src, dst, regex=False, field=None, fold=True):
"Find and replace fields in a fact." "Find and replace fields in a fact."
mmap = {} mmap = {}
if field: if field:
for m in deck.models().values(): for m in deck.models.all():
for f in m.fields: for f in m['flds']:
if f['name'] == field: if f['name'] == field:
mmap[m.id] = f['ord'] mmap[m['id']] = f['ord']
if not mmap: if not mmap:
return 0 return 0
# find and gather replacements # find and gather replacements
@ -393,7 +394,7 @@ def findReplace(deck, fids, src, dst, regex=False, field=None, fold=True):
# does it match? # does it match?
sflds = splitFields(flds) sflds = splitFields(flds)
if field: if field:
ord = mmap[mid] ord = mmap[str(mid)]
sflds[ord] = repl(sflds[ord]) sflds[ord] = repl(sflds[ord])
else: else:
for c in range(len(sflds)): for c in range(len(sflds)):

View file

@ -71,9 +71,9 @@ def _latexFromHtml(deck, latex):
def _buildImg(deck, latex, fname, model): def _buildImg(deck, latex, fname, model):
# add header/footer # add header/footer
latex = (model.conf["latexPre"] + "\n" + latex = (model["latexPre"] + "\n" +
latex + "\n" + latex + "\n" +
model.conf["latexPost"]) model["latexPost"])
# write into a temp file # write into a temp file
log = open(namedtmp("latex_log.txt"), "w") log = open(namedtmp("latex_log.txt"), "w")
texfile = file(namedtmp("tmp.tex"), "w") texfile = file(namedtmp("tmp.tex"), "w")

View file

@ -2,19 +2,19 @@
# 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
import simplejson import simplejson, copy
from anki.utils import intTime, hexifyID, joinFields, splitFields, ids2str, \ from anki.utils import intTime, hexifyID, joinFields, splitFields, ids2str, \
timestampID timestampID
from anki.lang import _ from anki.lang import _
# Models # Models
########################################################################## ##########################################################################
# gid may point to non-existent group
defaultConf = { # careful not to add any lists/dicts/etc here, as they aren't deep copied
defaultModel = {
'css': "",
'sortf': 0, 'sortf': 0,
'gid': 1, 'gid': 1,
'tags': [],
'clozectx': False, 'clozectx': False,
'latexPre': """\ 'latexPre': """\
\\documentclass[12pt]{article} \\documentclass[12pt]{article}
@ -56,82 +56,132 @@ defaultTemplate = {
'gid': None, 'gid': None,
} }
class Model(object): class ModelRegistry(object):
def __init__(self, deck, id=None): # Saving/loading registry
#############################################################
def __init__(self, deck):
self.deck = deck self.deck = deck
if id:
self.id = id
self.load()
else:
self.id = timestampID(deck.db, "models")
self.name = u""
self.conf = defaultConf.copy()
self.css = ""
self.fields = []
self.templates = []
def load(self): def load(self, json):
(self.mod, "Load registry from JSON."
self.name, self.changed = False
self.fields, self.models = simplejson.loads(json)
self.templates,
self.conf, def save(self, m=None):
self.css) = self.deck.db.first(""" "Mark M modified if provided, and schedule registry flush."
select mod, name, flds, tmpls, conf, css from models where id = ?""", self.id) if m:
self.fields = simplejson.loads(self.fields) m['mod'] = intTime()
self.templates = simplejson.loads(self.templates) m['css'] = self._css(m)
self.conf = simplejson.loads(self.conf) self.changed = True
def flush(self): def flush(self):
self.mod = intTime() "Flush the registry if any models were changed."
self.css = self.genCSS() if self.changed:
ret = self.deck.db.execute(""" self.deck.db.execute("update deck set models = ?",
insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""", simplejson.dumps(self.models))
self.id, self.mod, self.name,
simplejson.dumps(self.fields),
simplejson.dumps(self.templates),
simplejson.dumps(self.conf),
self.css)
self.id = ret.lastrowid
def fids(self): # Retrieving and creating models
#############################################################
def current(self):
"Get current model."
return self.get(self.deck.conf['currentModelId'])
def get(self, id):
"Get model with ID."
return self.models[str(id)]
def all(self):
"Get all models."
return self.models.values()
def byName(self, name):
"Get model with NAME."
for m in self.models.values():
if m['name'].lower() == name.lower():
return m
def new(self, name):
"Create a new model, save it in the registry, and return it."
# caller should call save() after modifying
m = defaultModel.copy()
m['name'] = name
m['mod'] = intTime()
m['flds'] = []
m['tmpls'] = []
m['tags'] = []
return self._add(m)
def del_(self, m):
"Delete model, and all its cards/facts."
self.deck.modSchema()
# delete facts/cards
self.deck.delCards(self.deck.db.list("""
select id from cards where fid in (select id from facts where mid = ?)""",
m['id']))
# then the model
del self.models[m['id']]
self.save()
# GUI should ensure last model is not deleted
if self.deck.conf['currentModelId'] == m['id']:
self.deck.conf['currentModelId'] = int(self.models.keys()[0])
def _add(self, m):
self._setID(m)
self.models[m['id']] = m
self.save(m)
self.deck.conf['currentModelId'] = m['id']
return m
def _setID(self, m):
while 1:
id = str(intTime(1000))
if id not in self.models:
break
m['id'] = id
# Tools
##################################################
def fids(self, m):
"Fact ids for M."
return self.deck.db.list( return self.deck.db.list(
"select id from facts where mid = ?", self.id) "select id from facts where mid = ?", m['id'])
def useCount(self): def useCount(self, m):
"Number of fact using M."
return self.deck.db.scalar( return self.deck.db.scalar(
"select count() from facts where mid = ?", self.id) "select count() from facts where mid = ?", m['id'])
def css(self):
"CSS for all models."
return "\n".join([m['css'] for m in self.all()])
# Copying # Copying
################################################## ##################################################
def copy(self): def copy(self, m):
"Copy, flush and return." "Copy, save and return."
new = Model(self.deck, self.id) m2 = copy.deepcopy(m)
new.id = None m2['name'] = _("%s copy") % m2['name']
new.name += _(" copy") return self._add(m2)
new.fields = [f.copy() for f in self.fields]
new.templates = [t.copy() for t in self.templates]
new.flush()
return new
# CSS generation # CSS generation
################################################## ##################################################
def genCSS(self): def _css(self, m):
if not self.id:
return ""
# fields # fields
css = "".join(self._fieldCSS( css = "".join(self._fieldCSS(
".fm%s-%s" % (hexifyID(self.id), hexifyID(f['ord'])), ".fm%s-%s" % (hexifyID(m['id']), hexifyID(f['ord'])),
(f['font'], f['qsize'], f['qcol'], f['rtl'], f['pre'])) (f['font'], f['qsize'], f['qcol'], f['rtl'], f['pre']))
for f in self.fields) for f in m['flds'])
# templates # templates
css += "".join(".cm%s-%s {text-align:%s;background:%s}\n" % ( css += "".join(".cm%s-%s {text-align:%s;background:%s}\n" % (
hexifyID(self.id), hexifyID(t['ord']), hexifyID(m['id']), hexifyID(t['ord']),
("center", "left", "right")[t['align']], t['bg']) ("center", "left", "right")[t['align']], t['bg'])
for t in self.templates) for t in m['tmpls'])
return css return css
def _rewriteFont(self, font): def _rewriteFont(self, font):
@ -158,64 +208,66 @@ insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""",
# Fields # Fields
################################################## ##################################################
def fieldMap(self): def newField(self, name):
f = defaultField.copy()
f['name'] = name
return f
def fieldMap(self, m):
"Mapping of field name -> (ord, field)." "Mapping of field name -> (ord, field)."
return dict((f['name'], (f['ord'], f)) for f in self.fields) return dict((f['name'], (f['ord'], f)) for f in m['flds'])
def sortIdx(self): def sortIdx(self, m):
return self.conf['sortf'] return m['sortf']
def setSortIdx(self, idx): def setSortIdx(self, m, idx):
assert idx >= 0 and idx < len(self.fields) assert idx >= 0 and idx < len(m['flds'])
self.deck.modSchema() self.deck.modSchema()
self.conf['sortf'] = idx m['sortf'] = idx
self.deck.updateFieldCache(self.fids(), csum=False) self.deck.updateFieldCache(self.fids(m), csum=False)
self.flush() self.save(m)
def newField(self): def addField(self, m, field):
return defaultField.copy() m['flds'].append(field)
self._updateFieldOrds(m)
def addField(self, field): self.save(m)
self.fields.append(field)
self._updateFieldOrds()
self.flush()
def add(fields): def add(fields):
fields.append("") fields.append("")
return fields return fields
self._transformFields(add) self._transformFields(m, add)
def delField(self, field): def delField(self, m, field):
idx = self.fields.index(field) idx = m['flds'].index(field)
self.fields.remove(field) m['flds'].remove(field)
self._updateFieldOrds() self._updateFieldOrds(m)
def delete(fields): def delete(fields):
del fields[idx] del fields[idx]
return fields return fields
self._transformFields(delete) self._transformFields(m, delete)
if idx == self.sortIdx(): if idx == self.sortIdx(m):
# need to rebuild # need to rebuild
self.deck.updateFieldCache(self.fids(), csum=False) self.deck.updateFieldCache(self.fids(m), csum=False)
# flushes # saves
self.renameField(field, None) self.renameField(m, field, None)
def moveField(self, field, idx): def moveField(self, m, field, idx):
oldidx = self.fields.index(field) oldidx = m['flds'].index(field)
if oldidx == idx: if oldidx == idx:
return return
self.fields.remove(field) m['flds'].remove(field)
self.fields.insert(idx, field) m['flds'].insert(idx, field)
self._updateFieldOrds() self._updateFieldOrds(m)
self.flush() self.save(m)
def move(fields, oldidx=oldidx): def move(fields, oldidx=oldidx):
val = fields[oldidx] val = fields[oldidx]
del fields[oldidx] del fields[oldidx]
fields.insert(idx, val) fields.insert(idx, val)
return fields return fields
self._transformFields(move) self._transformFields(m, move)
def renameField(self, field, newName): def renameField(self, m, field, newName):
self.deck.modSchema() self.deck.modSchema()
for t in self.templates: for t in m['tmpls']:
types = ("{{%s}}", "{{text:%s}}", "{{#%s}}", types = ("{{%s}}", "{{text:%s}}", "{{#%s}}",
"{{^%s}}", "{{/%s}}") "{{^%s}}", "{{/%s}}")
for type in types: for type in types:
@ -226,77 +278,79 @@ insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""",
repl = "" repl = ""
t[fmt] = t[fmt].replace(type%field['name'], repl) t[fmt] = t[fmt].replace(type%field['name'], repl)
field['name'] = newName field['name'] = newName
self.flush() self.save(m)
def _updateFieldOrds(self): def _updateFieldOrds(self, m):
for c, f in enumerate(self.fields): for c, f in enumerate(m['flds']):
f['ord'] = c f['ord'] = c
def _transformFields(self, fn): def _transformFields(self, m, fn):
self.deck.modSchema() self.deck.modSchema()
r = [] r = []
for (id, flds) in self.deck.db.execute( for (id, flds) in self.deck.db.execute(
"select id, flds from facts where mid = ?", self.id): "select id, flds from facts where mid = ?", m['id']):
r.append((joinFields(fn(splitFields(flds))), id)) r.append((joinFields(fn(splitFields(flds))), id))
self.deck.db.executemany("update facts set flds = ? where id = ?", r) self.deck.db.executemany("update facts set flds = ? where id = ?", r)
# Templates # Templates
################################################## ##################################################
def newTemplate(self): def newTemplate(self, name):
return defaultTemplate.copy() t = defaultTemplate.copy()
t['name'] = name
return t
def addTemplate(self, template): def addTemplate(self, m, template):
self.deck.modSchema() self.deck.modSchema()
self.templates.append(template) m['tmpls'].append(template)
self._updateTemplOrds() self._updateTemplOrds(m)
self.flush() self.save(m)
def delTemplate(self, template): def delTemplate(self, m, template):
self.deck.modSchema() self.deck.modSchema()
ord = self.templates.index(template) ord = m['tmpls'].index(template)
cids = self.deck.db.list(""" cids = self.deck.db.list("""
select c.id from cards c, facts f where c.fid=f.id and mid = ? and ord = ?""", select c.id from cards c, facts f where c.fid=f.id and mid = ? and ord = ?""",
self.id, ord) m['id'], ord)
self.deck.delCards(cids) self.deck.delCards(cids)
# shift ordinals # shift ordinals
self.deck.db.execute(""" self.deck.db.execute("""
update cards set ord = ord - 1 where fid in (select id from facts update cards set ord = ord - 1 where fid in (select id from facts
where mid = ?) and ord > ?""", self.id, ord) where mid = ?) and ord > ?""", m['id'], ord)
self.templates.remove(template) m['tmpls'].remove(template)
self._updateTemplOrds() self._updateTemplOrds(m)
self.flush() self.save(m)
def _updateTemplOrds(self): def _updateTemplOrds(self, m):
for c, t in enumerate(self.templates): for c, t in enumerate(m['tmpls']):
t['ord'] = c t['ord'] = c
def moveTemplate(self, template, idx): def moveTemplate(self, m, template, idx):
oldidx = self.templates.index(template) oldidx = m['tmpls'].index(template)
if oldidx == idx: if oldidx == idx:
return return
oldidxs = dict((id(t), t['ord']) for t in self.templates) oldidxs = dict((id(t), t['ord']) for t in m['tmpls'])
self.templates.remove(template) m['tmpls'].remove(template)
self.templates.insert(idx, template) m['tmpls'].insert(idx, template)
self._updateTemplOrds() self._updateTemplOrds(m)
# generate change map # generate change map
map = [] map = []
for t in self.templates: for t in m['tmpls']:
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.flush() self.save(m)
self.deck.db.execute(""" self.deck.db.execute("""
update cards set ord = (case %s end) where fid in ( update cards set ord = (case %s end) where fid in (
select id from facts where mid = ?)""" % " ".join(map), self.id) select id from facts where mid = ?)""" % " ".join(map), m['id'])
# Model changing # Model changing
########################################################################## ##########################################################################
# - maps are ord->ord, and there should not be duplicate targets # - maps are ord->ord, and there should not be duplicate targets
# - newModel should be self if model is not changing # - newModel should be self if model is not changing
def changeModel(self, fids, newModel, fmap, cmap): def change(self, m, fids, newModel, fmap, cmap):
self.deck.modSchema() self.deck.modSchema()
assert newModel.id == self.id or (fmap and cmap) assert newModel['id'] == m['id'] or (fmap and cmap)
if fmap: if fmap:
self._changeFacts(fids, newModel, fmap) self._changeFacts(fids, newModel, fmap)
if cmap: if cmap:
@ -304,7 +358,7 @@ select id from facts where mid = ?)""" % " ".join(map), self.id)
def _changeFacts(self, fids, newModel, map): def _changeFacts(self, fids, newModel, map):
d = [] d = []
nfields = len(newModel.fields) nfields = len(newModel['flds'])
for (fid, flds) in self.deck.db.execute( for (fid, flds) in self.deck.db.execute(
"select id, flds from facts where id in "+ids2str(fids)): "select id, flds from facts where id in "+ids2str(fids)):
newflds = {} newflds = {}
@ -315,7 +369,7 @@ select id from facts where mid = ?)""" % " ".join(map), self.id)
for c in range(nfields): for c in range(nfields):
flds.append(newflds.get(c, "")) flds.append(newflds.get(c, ""))
flds = joinFields(flds) flds = joinFields(flds)
d.append(dict(fid=fid, flds=flds, mid=newModel.id)) d.append(dict(fid=fid, flds=flds, mid=newModel['id']))
self.deck.db.executemany( self.deck.db.executemany(
"update facts set flds=:flds, mid=:mid where id = :fid", d) "update facts set flds=:flds, mid=:mid where id = :fid", d)
self.deck.updateFieldCache(fids) self.deck.updateFieldCache(fids)

View file

@ -47,7 +47,7 @@ class CardStats(object):
self.addLine(_("Total Time"), self.time(total)) self.addLine(_("Total Time"), self.time(total))
elif c.queue == 0: elif c.queue == 0:
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.groupName(c.gid)) self.addLine(_("Current Group"), self.deck.groupName(c.gid))
self.addLine(_("Initial Group"), self.deck.groupName(c.fact().gid)) self.addLine(_("Initial Group"), self.deck.groupName(c.fact().gid))

View file

@ -2,7 +2,6 @@
# 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.models import Model
from anki.lang import _ from anki.lang import _
models = [] models = []
@ -10,55 +9,50 @@ models = []
# Basic # Basic
########################################################################## ##########################################################################
def BasicModel(deck): def addBasicModel(deck):
m = Model(deck) mm = deck.models
m.name = _("Basic") m = mm.new(_("Basic"))
fm = m.newField() fm = mm.newField(_("Front"))
fm['name'] = _("Front")
fm['req'] = True fm['req'] = True
fm['uniq'] = True fm['uniq'] = True
m.addField(fm) mm.addField(m, fm)
fm = m.newField() fm = mm.newField(_("Back"))
fm['name'] = _("Back") mm.addField(m, fm)
m.addField(fm) t = mm.newTemplate(_("Forward"))
t = m.newTemplate()
t['name'] = _("Forward")
t['qfmt'] = "{{" + _("Front") + "}}" t['qfmt'] = "{{" + _("Front") + "}}"
t['afmt'] = "{{" + _("Back") + "}}" t['afmt'] = "{{" + _("Back") + "}}"
m.addTemplate(t) mm.addTemplate(m, t)
t = m.newTemplate() t = mm.newTemplate(_("Reverse"))
t['name'] = _("Reverse")
t['qfmt'] = "{{" + _("Back") + "}}" t['qfmt'] = "{{" + _("Back") + "}}"
t['afmt'] = "{{" + _("Front") + "}}" t['afmt'] = "{{" + _("Front") + "}}"
t['actv'] = False t['actv'] = False
m.addTemplate(t) mm.addTemplate(m, t)
mm.save(m)
return m return m
models.append((_("Basic"), BasicModel)) models.append((_("Basic"), addBasicModel))
# Cloze # Cloze
########################################################################## ##########################################################################
def ClozeModel(deck): def addClozeModel(deck):
m = Model(deck) mm = deck.models
m.name = _("Cloze") m = mm.new(_("Cloze"))
fm = m.newField() fm = mm.newField(_("Text"))
fm['name'] = _("Text")
fm['req'] = True fm['req'] = True
fm['uniq'] = True fm['uniq'] = True
m.addField(fm) mm.addField(m, fm)
fm = m.newField() fm = mm.newField(_("Notes"))
fm['name'] = _("Notes") mm.addField(m, fm)
m.addField(fm)
for i in range(8): for i in range(8):
n = i+1 n = i+1
t = m.newTemplate() t = mm.newTemplate(_("Cloze") + " %d" % n)
t['name'] = _("Cloze") + " %d" % n
t['qfmt'] = ("{{#cloze:%d:Text}}<br>{{cloze:%d:%s}}<br>"+ t['qfmt'] = ("{{#cloze:%d:Text}}<br>{{cloze:%d:%s}}<br>"+
"{{/cloze:%d:Text}}") % (n, n, _("Text"), n) "{{/cloze:%d:Text}}") % (n, n, _("Text"), n)
t['afmt'] = ("{{cloze:%d:" + _("Text") + "}}") % n t['afmt'] = ("{{cloze:%d:" + _("Text") + "}}") % n
t['afmt'] += "<br>{{" + _("Notes") + "}}" t['afmt'] += "<br>{{" + _("Notes") + "}}"
m.addTemplate(t) mm.addTemplate(m, t)
mm.save(m)
return m return m
models.append((_("Cloze"), ClozeModel)) models.append((_("Cloze"), addClozeModel))

View file

@ -9,7 +9,7 @@ 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.deck import _Deck
from anki.stdmodels import BasicModel, ClozeModel from anki.stdmodels import addBasicModel, addClozeModel
from anki.errors import AnkiError from anki.errors import AnkiError
from anki.hooks import runHook from anki.hooks import runHook
@ -34,8 +34,8 @@ def Deck(path, queue=True, lock=True):
_upgradeDeck(deck, ver) _upgradeDeck(deck, ver)
elif create: elif create:
# add in reverse order so basic is default # add in reverse order so basic is default
deck.addModel(ClozeModel(deck)) addClozeModel(deck)
deck.addModel(BasicModel(deck)) addBasicModel(deck)
deck.save() deck.save()
if lock: if lock:
deck.lock() deck.lock()
@ -67,9 +67,9 @@ create table if not exists deck (
lastSync integer not null, lastSync integer not null,
qconf text not null, qconf text not null,
conf text not null, conf text not null,
models text not null,
groups text not null, groups text not null,
gconf text not null, gconf text not null
data text not null
); );
create table if not exists cards ( create table if not exists cards (
@ -108,16 +108,6 @@ create table if not exists fsums (
csum integer not null csum integer not null
); );
create table if not exists models (
id integer primary key,
mod integer not null,
name text not null,
flds text not null,
tmpls text not null,
conf text not null,
css text not null
);
create table if not exists graves ( create table if not exists graves (
time integer not null, time integer not null,
oid integer not null, oid integer not null,
@ -148,13 +138,14 @@ values(1,0,0,0,%(v)s,0,'',0,'','','','','');
import anki.groups import anki.groups
if setDeckConf: if setDeckConf:
db.execute(""" db.execute("""
update deck set qconf = ?, conf = ?, groups = ?, gconf = ?, data = ?""", update deck set qconf = ?, conf = ?, models = ?, groups = ?, gconf = ?""",
simplejson.dumps(anki.deck.defaultQconf), simplejson.dumps(anki.deck.defaultQconf),
simplejson.dumps(anki.deck.defaultConf), simplejson.dumps(anki.deck.defaultConf),
"{}",
simplejson.dumps({'1': {'name': _("Default"), 'conf': 1, simplejson.dumps({'1': {'name': _("Default"), 'conf': 1,
'mod': intTime()}}), 'mod': intTime()}}),
simplejson.dumps({'1': anki.groups.defaultConf}), simplejson.dumps({'1': anki.groups.defaultConf}))
"{}")
def _updateIndices(db): def _updateIndices(db):
"Add indices to the DB." "Add indices to the DB."
@ -494,7 +485,7 @@ order by modelId, ordinal"""):
def _fixupModels(deck): def _fixupModels(deck):
# rewrite model/template/field ids # rewrite model/template/field ids
models = deck.models() models = deck.models.all()
deck.db.execute("delete from models") deck.db.execute("delete from models")
times = {} times = {}
for c, m in enumerate(models.values()): for c, m in enumerate(models.values()):

View file

@ -171,7 +171,7 @@ def entsToTxt(html):
############################################################################## ##############################################################################
def hexifyID(id): def hexifyID(id):
return "%x" % id return "%x" % int(id)
def dehexifyID(id): def dehexifyID(id):
return int(id, 16) return int(id, 16)

View file

@ -12,7 +12,7 @@ def test_genCards():
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
deck.addFact(f) deck.addFact(f)
cards = deck.genCards(f, f.model().templates) cards = deck.genCards(f, f.model()['tmpls'])
assert len(cards) == 1 assert len(cards) == 1
assert cards[0].ord == 1 assert cards[0].ord == 1
assert deck.cardCount() == 2 assert deck.cardCount() == 2
@ -23,7 +23,7 @@ def test_genCards():
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
deck.addFact(f) deck.addFact(f)
cards = deck.genCards(f, f.model().templates) cards = deck.genCards(f, f.model()['tmpls'])
assert deck.cardCount() == 4 assert deck.cardCount() == 4
c = deck.db.list("select due from cards where fid = ?", f.id) c = deck.db.list("select due from cards where fid = ?", f.id)
assert c[0] == c[1] assert c[0] == c[1]

View file

@ -2,7 +2,7 @@
import os, re, datetime import os, re, datetime
from tests.shared import assertException, getEmptyDeck, testDir from tests.shared import assertException, getEmptyDeck, testDir
from anki.stdmodels import BasicModel from anki.stdmodels import addBasicModel
from anki import Deck from anki import Deck
@ -53,8 +53,8 @@ def test_factAddDelete():
f = deck.newFact() f = deck.newFact()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
m = f.model() m = f.model()
m.templates[1]['actv'] = True m['tmpls'][1]['actv'] = True
m.flush() deck.models.save(m)
n = deck.addFact(f) n = deck.addFact(f)
assert n == 2 assert n == 2
# check q/a generation # check q/a generation
@ -65,7 +65,7 @@ def test_factAddDelete():
assert not p assert not p
# now let's make a duplicate and test uniqueness # now let's make a duplicate and test uniqueness
f2 = deck.newFact() f2 = deck.newFact()
f2.model().fields[1]['req'] = True f2.model()['flds'][1]['req'] = True
f2['Front'] = u"one"; f2['Back'] = u"" f2['Front'] = u"one"; f2['Back'] = u""
p = f2.problems() p = f2.problems()
assert p[0] == "unique" assert p[0] == "unique"
@ -102,15 +102,15 @@ def test_fieldChecksum():
"select csum from fsums") == int("4b0e5a4c", 16) "select csum from fsums") == int("4b0e5a4c", 16)
# turning off unique and modifying the fact should delete the sum # turning off unique and modifying the fact should delete the sum
m = f.model() m = f.model()
m.fields[0]['uniq'] = False m['flds'][0]['uniq'] = False
m.flush() deck.models.save(m)
f.flush() f.flush()
assert deck.db.scalar( assert deck.db.scalar(
"select count() from fsums") == 0 "select count() from fsums") == 0
# and turning on both should ensure two checksums generated # and turning on both should ensure two checksums generated
m.fields[0]['uniq'] = True m['flds'][0]['uniq'] = True
m.fields[1]['uniq'] = True m['flds'][1]['uniq'] = True
m.flush() deck.models.save(m)
f.flush() f.flush()
assert deck.db.scalar( assert deck.db.scalar(
"select count() from fsums") == 2 "select count() from fsums") == 2
@ -190,8 +190,8 @@ def test_addDelTags():
def test_timestamps(): def test_timestamps():
deck = getEmptyDeck() deck = getEmptyDeck()
assert len(deck.models()) == 2 assert len(deck.models.models) == 2
for i in range(100): for i in range(100):
deck.addModel(BasicModel(deck)) addBasicModel(deck)
assert len(deck.models()) == 102 assert len(deck.models.models) == 102

View file

@ -25,7 +25,7 @@ def test_findCards():
f = deck.newFact() f = deck.newFact()
f['Front'] = u'template test' f['Front'] = u'template test'
f['Back'] = u'foo bar' f['Back'] = u'foo bar'
f.model().templates[1]['actv'] = True f.model()['tmpls'][1]['actv'] = True
deck.addFact(f) deck.addFact(f)
latestCardIds = [c.id for c in f.cards()] latestCardIds = [c.id for c in f.cards()]
# tag searches # tag searches

View file

@ -2,7 +2,6 @@
import os import os
from tests.shared import assertException, getEmptyDeck from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
from anki.utils import stripHTML, intTime from anki.utils import stripHTML, intTime
from anki.hooks import addHook from anki.hooks import addHook

View file

@ -1,7 +1,6 @@
# coding: utf-8 # coding: utf-8
from tests.shared import getEmptyDeck from tests.shared import getEmptyDeck
from anki.models import Model
from anki.utils import stripHTML from anki.utils import stripHTML
def test_modelDelete(): def test_modelDelete():
@ -11,20 +10,20 @@ def test_modelDelete():
f['Back'] = u'2' f['Back'] = u'2'
deck.addFact(f) deck.addFact(f)
assert deck.cardCount() == 1 assert deck.cardCount() == 1
deck.delModel(deck.conf['currentModelId']) deck.models.del_(deck.models.get(deck.conf['currentModelId']))
assert deck.cardCount() == 0 assert deck.cardCount() == 0
def test_modelCopy(): def test_modelCopy():
deck = getEmptyDeck() deck = getEmptyDeck()
m = deck.currentModel() m = deck.models.current()
m2 = m.copy() m2 = deck.models.copy(m)
assert m2.name == "Basic copy" assert m2['name'] == "Basic copy"
assert m2.id != m.id assert m2['id'] != m['id']
assert len(m2.fields) == 2 assert len(m2['flds']) == 2
assert len(m.fields) == 2 assert len(m['flds']) == 2
assert len(m2.fields) == len(m.fields) assert len(m2['flds']) == len(m['flds'])
assert len(m.templates) == 2 assert len(m['tmpls']) == 2
assert len(m2.templates) == 2 assert len(m2['tmpls']) == 2
def test_fields(): def test_fields():
d = getEmptyDeck() d = getEmptyDeck()
@ -32,50 +31,50 @@ def test_fields():
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
d.addFact(f) d.addFact(f)
m = d.currentModel() m = d.models.current()
# make sure renaming a field updates the templates # make sure renaming a field updates the templates
m.renameField(m.fields[0], "NewFront") d.models.renameField(m, m['flds'][0], "NewFront")
assert m.templates[0]['qfmt'] == "{{NewFront}}" assert m['tmpls'][0]['qfmt'] == "{{NewFront}}"
# add a field # add a field
f = m.newField() f = d.models.newField(m)
f['name'] = "foo" f['name'] = "foo"
m.addField(f) d.models.addField(m, f)
assert d.getFact(m.fids()[0]).fields == ["1", "2", ""] assert d.getFact(d.models.fids(m)[0]).fields == ["1", "2", ""]
# rename it # rename it
m.renameField(f, "bar") d.models.renameField(m, f, "bar")
assert d.getFact(m.fids()[0])['bar'] == '' assert d.getFact(d.models.fids(m)[0])['bar'] == ''
# delete back # delete back
m.delField(m.fields[1]) d.models.delField(m, m['flds'][1])
assert d.getFact(m.fids()[0]).fields == ["1", ""] assert d.getFact(d.models.fids(m)[0]).fields == ["1", ""]
# move 0 -> 1 # move 0 -> 1
m.moveField(m.fields[0], 1) d.models.moveField(m, m['flds'][0], 1)
assert d.getFact(m.fids()[0]).fields == ["", "1"] assert d.getFact(d.models.fids(m)[0]).fields == ["", "1"]
# move 1 -> 0 # move 1 -> 0
m.moveField(m.fields[1], 0) d.models.moveField(m, m['flds'][1], 0)
assert d.getFact(m.fids()[0]).fields == ["1", ""] assert d.getFact(d.models.fids(m)[0]).fields == ["1", ""]
# add another and put in middle # add another and put in middle
f = m.newField() f = d.models.newField(m)
f['name'] = "baz" f['name'] = "baz"
m.addField(f) d.models.addField(m, f)
f = d.getFact(m.fids()[0]) f = d.getFact(d.models.fids(m)[0])
f['baz'] = "2" f['baz'] = "2"
f.flush() f.flush()
assert d.getFact(m.fids()[0]).fields == ["1", "", "2"] assert d.getFact(d.models.fids(m)[0]).fields == ["1", "", "2"]
# move 2 -> 1 # move 2 -> 1
m.moveField(m.fields[2], 1) d.models.moveField(m, m['flds'][2], 1)
assert d.getFact(m.fids()[0]).fields == ["1", "2", ""] assert d.getFact(d.models.fids(m)[0]).fields == ["1", "2", ""]
# move 0 -> 2 # move 0 -> 2
m.moveField(m.fields[0], 2) d.models.moveField(m, m['flds'][0], 2)
assert d.getFact(m.fids()[0]).fields == ["2", "", "1"] assert d.getFact(d.models.fids(m)[0]).fields == ["2", "", "1"]
# move 0 -> 1 # move 0 -> 1
m.moveField(m.fields[0], 1) d.models.moveField(m, m['flds'][0], 1)
assert d.getFact(m.fids()[0]).fields == ["", "2", "1"] assert d.getFact(d.models.fids(m)[0]).fields == ["", "2", "1"]
def test_templates(): def test_templates():
d = getEmptyDeck() d = getEmptyDeck()
m = d.currentModel() m = d.models.current()
m.templates[1]['actv'] = True m['tmpls'][1]['actv'] = True
m.flush() d.models.save(m)
f = d.newFact() f = d.newFact()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -86,12 +85,12 @@ def test_templates():
assert c.ord == 0 assert c.ord == 0
assert c2.ord == 1 assert c2.ord == 1
# switch templates # switch templates
m.moveTemplate(c.template(), 1) d.models.moveTemplate(m, c.template(), 1)
c.load(); c2.load() c.load(); c2.load()
assert c.ord == 1 assert c.ord == 1
assert c2.ord == 0 assert c2.ord == 0
# removing a template should delete its cards # removing a template should delete its cards
m.delTemplate(m.templates[0]) d.models.delTemplate(m, m['tmpls'][0])
assert d.cardCount() == 1 assert d.cardCount() == 1
# and should have updated the other cards' ordinals # and should have updated the other cards' ordinals
c = f.cards()[0] c = f.cards()[0]
@ -100,9 +99,9 @@ def test_templates():
def test_text(): def test_text():
d = getEmptyDeck() d = getEmptyDeck()
m = d.currentModel() m = d.models.current()
m.templates[0]['qfmt'] = "{{text:Front}}" m['tmpls'][0]['qfmt'] = "{{text:Front}}"
m.flush() d.models.save(m)
f = d.newFact() f = d.newFact()
f['Front'] = u'hello<b>world' f['Front'] = u'hello<b>world'
d.addFact(f) d.addFact(f)
@ -110,9 +109,9 @@ def test_text():
def test_cloze(): def test_cloze():
d = getEmptyDeck() d = getEmptyDeck()
d.conf['currentModelId'] = d.modelId("Cloze") d.conf['currentModelId'] = d.models.byName("Cloze")['id']
f = d.newFact() f = d.newFact()
assert f.model().name == "Cloze" assert f.model()['name'] == "Cloze"
# a cloze model with no clozes is empty # a cloze model with no clozes is empty
f['Text'] = u'nothing' f['Text'] = u'nothing'
assert d.addFact(f) == 0 assert d.addFact(f) == 0
@ -124,7 +123,7 @@ def test_cloze():
assert "<span class=cloze>world</span>" in f.cards()[0].a() assert "<span class=cloze>world</span>" in f.cards()[0].a()
assert "hello <span class=cloze>world</span>" not in f.cards()[0].a() assert "hello <span class=cloze>world</span>" not in f.cards()[0].a()
# check context works too # check context works too
f.model().conf['clozectx'] = True f.model()['clozectx'] = True
assert "hello <span class=cloze>world</span>" in f.cards()[0].a() assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
# and with a comment # and with a comment
f = d.newFact() f = d.newFact()
@ -143,16 +142,16 @@ def test_cloze():
assert "world <span class=cloze>bar</span>" in c2.a() assert "world <span class=cloze>bar</span>" in c2.a()
# if there are multiple answers for a single cloze, they are given in a # if there are multiple answers for a single cloze, they are given in a
# list # list
f.model().conf['clozectx'] = False f.model()['clozectx'] = False
f = d.newFact() f = d.newFact()
f['Text'] = "a {{c1::b}} {{c1::c}}" f['Text'] = "a {{c1::b}} {{c1::c}}"
assert d.addFact(f) == 1 assert d.addFact(f) == 1
assert "<span class=cloze>b</span>, <span class=cloze>c</span>" in ( assert "<span class=cloze>b</span>, <span class=cloze>c</span>" in (
f.cards()[0].a()) f.cards()[0].a())
# clozes should be supported in sections too # clozes should be supported in sections too
m = d.currentModel() m = d.models.current()
m.templates[0]['qfmt'] = "{{#cloze:1:Text}}{{Notes}}{{/cloze:1:Text}}" m['tmpls'][0]['qfmt'] = "{{#cloze:1:Text}}{{Notes}}{{/cloze:1:Text}}"
m.flush() d.models.save(m)
f = d.newFact() f = d.newFact()
f['Text'] = "hello" f['Text'] = "hello"
f['Notes'] = "world" f['Notes'] = "world"
@ -162,18 +161,18 @@ def test_cloze():
def test_modelChange(): def test_modelChange():
deck = getEmptyDeck() deck = getEmptyDeck()
basic = deck.getModel(deck.modelId("Basic")) basic = deck.models.byName("Basic")
cloze = deck.getModel(deck.modelId("Cloze")) cloze = deck.models.byName("Cloze")
# enable second template and add a fact # enable second template and add a fact
basic.templates[1]['actv'] = True basic['tmpls'][1]['actv'] = True
basic.flush() deck.models.save(basic)
f = deck.newFact() f = deck.newFact()
f['Front'] = u'f' f['Front'] = u'f'
f['Back'] = u'b' f['Back'] = u'b'
deck.addFact(f) deck.addFact(f)
# switch fields # switch fields
map = {0: 1, 1: 0} map = {0: 1, 1: 0}
basic.changeModel([f.id], basic, map, None) deck.models.change(basic, [f.id], basic, map, None)
f.load() f.load()
assert f['Front'] == 'b' assert f['Front'] == 'b'
assert f['Back'] == 'f' assert f['Back'] == 'f'
@ -184,7 +183,7 @@ def test_modelChange():
assert stripHTML(c1.q()) == "f" assert stripHTML(c1.q()) == "f"
assert c0.ord == 0 assert c0.ord == 0
assert c1.ord == 1 assert c1.ord == 1
basic.changeModel([f.id], basic, None, map) deck.models.change(basic, [f.id], basic, None, map)
f.load(); c0.load(); c1.load() f.load(); c0.load(); c1.load()
assert stripHTML(c0.q()) == "f" assert stripHTML(c0.q()) == "f"
assert stripHTML(c1.q()) == "b" assert stripHTML(c1.q()) == "b"
@ -194,7 +193,7 @@ def test_modelChange():
assert f.cards()[0].id == c1.id assert f.cards()[0].id == c1.id
# delete first card # delete first card
map = {0: None, 1: 1} map = {0: None, 1: 1}
basic.changeModel([f.id], basic, None, map) deck.models.change(basic, [f.id], basic, None, map)
f.load() f.load()
c0.load() c0.load()
try: try:
@ -206,7 +205,7 @@ def test_modelChange():
# an unmapped field becomes blank # an unmapped field becomes blank
assert f['Front'] == 'b' assert f['Front'] == 'b'
assert f['Back'] == 'f' assert f['Back'] == 'f'
basic.changeModel([f.id], basic, map, None) deck.models.change(basic, [f.id], basic, map, None)
f.load() f.load()
assert f['Front'] == '' assert f['Front'] == ''
assert f['Back'] == 'f' assert f['Back'] == 'f'
@ -215,12 +214,21 @@ def test_modelChange():
f['Front'] = u'f2' f['Front'] = u'f2'
f['Back'] = u'b2' f['Back'] = u'b2'
deck.addFact(f) deck.addFact(f)
assert basic.useCount() == 2 assert deck.models.useCount(basic) == 2
assert cloze.useCount() == 0 assert deck.models.useCount(cloze) == 0
map = {0: 0, 1: 1} map = {0: 0, 1: 1}
basic.changeModel([f.id], cloze, map, map) deck.models.change(basic, [f.id], cloze, map, map)
f.load() f.load()
assert f['Text'] == "f2" assert f['Text'] == "f2"
assert f['Notes'] == "b2" assert f['Notes'] == "b2"
assert len(f.cards()) == 2 assert len(f.cards()) == 2
assert "b2" in f.cards()[0].a() assert "b2" in f.cards()[0].a()
def test_css():
deck = getEmptyDeck()
basic = deck.models.byName("Basic")
assert "arial" in basic['css']
assert "helvetica" not in basic['css']
basic['flds'][0]['font'] = "helvetica"
deck.models.save(basic)
assert "helvetica" in basic['css']

View file

@ -2,7 +2,6 @@
import time, copy import time, copy
from tests.shared import assertException, getEmptyDeck from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
from anki.utils import stripHTML, intTime from anki.utils import stripHTML, intTime
from anki.hooks import addHook from anki.hooks import addHook
@ -32,9 +31,9 @@ def test_new():
assert c.due >= t assert c.due >= t
# the default order should ensure siblings are not seen together, and # the default order should ensure siblings are not seen together, and
# should show all cards # should show all cards
m = d.currentModel() m = d.models.current()
m.templates[1]['actv'] = True m['tmpls'][1]['actv'] = True
m.flush() d.models.save(m)
f = d.newFact() f = d.newFact()
f['Front'] = u"2"; f['Back'] = u"2" f['Front'] = u"2"; f['Back'] = u"2"
d.addFact(f) d.addFact(f)
@ -50,15 +49,15 @@ def test_new():
def test_newOrder(): def test_newOrder():
d = getEmptyDeck() d = getEmptyDeck()
m = d.currentModel() m = d.models.current()
for i in range(50): for i in range(50):
t = m.newTemplate() t = d.models.newTemplate(m)
t['name'] = str(i) t['name'] = str(i)
t['qfmt'] = "{{Front}}" t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}" t['afmt'] = "{{Back}}"
t['actv'] = i > 25 t['actv'] = i > 25
m.addTemplate(t) d.models.addTemplate(m, t)
m.flush() d.models.save(m)
f = d.newFact() f = d.newFact()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -495,19 +494,19 @@ def test_cramLimits():
def test_adjIvl(): def test_adjIvl():
d = getEmptyDeck() d = getEmptyDeck()
# add two more templates and set second active # add two more templates and set second active
m = d.currentModel() m = d.models.current()
m.templates[1]['actv'] = True m['tmpls'][1]['actv'] = True
t = m.newTemplate() t = d.models.newTemplate(m)
t['name'] = "f2" t['name'] = "f2"
t['qfmt'] = "{{Front}}" t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}" t['afmt'] = "{{Back}}"
m.addTemplate(t) d.models.addTemplate(m, t)
t = m.newTemplate() t = d.models.newTemplate(m)
t['name'] = "f3" t['name'] = "f3"
t['qfmt'] = "{{Front}}" t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}" t['afmt'] = "{{Back}}"
m.addTemplate(t) d.models.addTemplate(m, t)
m.flush() d.models.save(m)
# create a new fact; it should have 4 cards # create a new fact; it should have 4 cards
f = d.newFact() f = d.newFact()
f['Front'] = "1"; f['Back'] = "1" f['Front'] = "1"; f['Back'] = "1"
@ -560,14 +559,14 @@ def test_adjIvl():
def test_ordcycle(): def test_ordcycle():
d = getEmptyDeck() d = getEmptyDeck()
# add two more templates and set second active # add two more templates and set second active
m = d.currentModel() m = d.models.current()
m.templates[1]['actv'] = True m['tmpls'][1]['actv'] = True
t = m.newTemplate() t = d.models.newTemplate(m)
t['name'] = "f2" t['name'] = "f2"
t['qfmt'] = "{{Front}}" t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}" t['afmt'] = "{{Back}}"
m.addTemplate(t) d.models.addTemplate(m, t)
m.flush() d.models.save(m)
# create a new fact; it should have 4 cards # create a new fact; it should have 4 cards
f = d.newFact() f = d.newFact()
f['Front'] = "1"; f['Back'] = "1" f['Front'] = "1"; f['Back'] = "1"

View file

@ -2,7 +2,6 @@
import time, copy, os import time, copy, os
from tests.shared import assertException, getEmptyDeck from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
from anki.utils import stripHTML, intTime from anki.utils import stripHTML, intTime
from anki.hooks import addHook from anki.hooks import addHook

View file

@ -6,7 +6,6 @@ from tests.shared import assertException
from anki.errors import * from anki.errors import *
from anki import Deck from anki import Deck
from anki.utils import intTime from anki.utils import intTime
from anki.stdmodels import BasicModel
from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy
from anki.sync import copyLocalMedia from anki.sync import copyLocalMedia
from anki.facts import Fact from anki.facts import Fact

View file

@ -2,8 +2,6 @@
import time import time
from tests.shared import assertException, getEmptyDeck from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
def test_op(): def test_op():
d = getEmptyDeck() d = getEmptyDeck()
# should have no undo by default # should have no undo by default