convert templates to a json object, and replace tid with ord

it's faster for us to parse another json string than pull a record from a
separate db table, and this makes templates and fields consistent
This commit is contained in:
Damien Elmes 2011-03-12 03:47:15 +09:00
parent 9cec4b2059
commit 93dcfceffe
9 changed files with 112 additions and 193 deletions

View file

@ -28,7 +28,7 @@ class Card(object):
self.id = id
self.load()
else:
# to flush, set fid, tid, and due
# to flush, set fid, ord, and due
self.id = None
self.gid = 1
self.crt = intTime()
@ -46,8 +46,8 @@ class Card(object):
def load(self):
(self.id,
self.fid,
self.tid,
self.gid,
self.ord,
self.crt,
self.mod,
self.type,
@ -71,8 +71,8 @@ insert or replace into cards values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
self.id,
self.fid,
self.tid,
self.gid,
self.ord,
self.crt,
self.mod,
self.type,

View file

@ -401,9 +401,8 @@ due > :now and due < :now""", now=time.time())
for template in cms:
card = anki.cards.Card(self)
card.fid = fact.id
card.tid = template.id
card.ord = template.ord
card.gid = template.conf['gid'] or gid
card.ord = template['ord']
card.gid = template['gid'] or gid
if isRandom:
card.due = due
else:
@ -418,19 +417,21 @@ due > :now and due < :now""", now=time.time())
def findTemplates(self, fact, checkActive=True):
"Return active, non-empty templates."
ok = []
for template in fact.model.templates:
if template.active or not checkActive:
# [cid, fid, mid, tid, gid, tags, flds, data]
data = [1, 1, fact.model.id, template.id, 1,
for c, template in enumerate(fact.model.templates):
if template['actv'] or not checkActive:
# [cid, fid, mid, gid, ord, tags, flds, data]
data = [1, 1, fact.model.id, 1, c,
"", fact.joinedFields(), ""]
now = self._renderQA(fact.model, template, "", data)
now = self._renderQA(fact.model, "", data)
data[6] = "\x1f".join([""]*len(fact._fields))
empty = self._renderQA(fact.model, template, "", data)
empty = self._renderQA(fact.model, "", data)
if now['q'] == empty['q']:
continue
if not template.conf['allowEmptyAns']:
if not template['emptyAns']:
if now['a'] == empty['a']:
continue
# add ordinal
template['ord'] = c
ok.append(template)
return ok
@ -647,7 +648,6 @@ 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)
self.db.execute("delete from templates where mid = ?", mid)
# GUI should ensure last model is not deleted
if self.conf['currentModelId'] == mid:
self.conf['currentModelId'] = self.db.scalar(
@ -861,18 +861,15 @@ where tid in %s""" % strids, now=time.time())
else:
raise Exception()
mods = {}
templs = {}
for m in self.allModels():
mods[m.id] = m
for t in m.templates:
templs[t.id] = t
groups = dict(self.db.all("select id, name from groups"))
return [self._renderQA(mods[row[2]], templs[row[3]], groups[row[4]], row)
return [self._renderQA(mods[row[2]], groups[row[3]], row)
for row in self._qaData(where)]
def _renderQA(self, model, template, gname, data, filters=True):
def _renderQA(self, model, gname, data, filters=True):
"Returns hash of id, question, answer."
# data is [cid, fid, mid, tid, gid, tags, flds, data]
# data is [cid, fid, mid, gid, ord, tags, flds, data]
# unpack fields and create dict
flist = data[6].split("\x1f")
fields = {}
@ -888,11 +885,12 @@ where tid in %s""" % strids, now=time.time())
fields[name] = ""
fields['Tags'] = data[5]
fields['Model'] = model.name
fields['Template'] = template.name
fields['Group'] = gname
template = model.templates[data[4]]
fields['Template'] = template['name']
# render q & a
d = dict(id=data[0])
for (type, format) in (("q", template.qfmt), ("a", template.afmt)):
for (type, format) in (("q", template['qfmt']), ("a", template['afmt'])):
# if filters:
# fields = runFilter("renderQA.pre", fields, , self)
html = anki.template.render(format, fields)
@ -903,12 +901,11 @@ where tid in %s""" % strids, now=time.time())
return d
def _qaData(self, where=""):
"Return [cid, fid, mid, tid, gid, tags, flds, data] db query"
"Return [cid, fid, mid, gid, ord, tags, flds, data] db query"
return self.db.execute("""
select c.id, f.id, m.id, t.id, g.id, f.tags, f.flds, f.data
from cards c, facts f, models m, templates t, groups g
where c.fid == f.id and f.mid == m.id and
c.tid = t.id and c.gid = g.id
select c.id, f.id, m.id, g.id, c.ord, f.tags, f.flds, f.data
from cards c, facts f, models m, groups g
where c.fid == f.id and f.mid == m.id and c.gid = g.id
%s""" % where)
# Field checksum bulk update

View file

@ -12,6 +12,31 @@ from anki.lang import _
defaultConf = {
}
defaultField = {
'name': "",
'rtl': False,
'req': False,
'uniq': False,
'font': "Arial",
'qsize': 20,
'esize': 20,
'qcol': "#fff",
'pre': True,
}
defaultTemplate = {
'name': "",
'actv': True,
'qfmt': "",
'afmt': "",
'hideQ': False,
'align': 0,
'bg': "#000",
'emptyAns': None,
'typeAns': None,
'gid': None
}
class Model(object):
def __init__(self, deck, id=None):
@ -32,34 +57,29 @@ class Model(object):
(self.mod,
self.name,
self.fields,
self.templates,
self.conf) = self.deck.db.first("""
select mod, name, flds, conf from models where id = ?""", self.id)
select mod, name, flds, tmpls, conf from models where id = ?""", self.id)
self.fields = simplejson.loads(self.fields)
self.templates = simplejson.loads(self.templates)
self.conf = simplejson.loads(self.conf)
self.loadTemplates()
def flush(self):
self.mod = intTime()
ret = self.deck.db.execute("""
insert or replace into models values (?, ?, ?, ?, ?, ?)""",
insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""",
self.id, self.mod, self.name,
simplejson.dumps(self.fields),
simplejson.dumps(self.templates),
simplejson.dumps(self.conf),
self.genCSS())
self.id = ret.lastrowid
[t._flush() for t in self.templates]
def _getID(self):
if not self.id:
# flush so we can get our DB id
self.flush()
return self.id
# Fields
##################################################
def newField(self):
return defaultFieldConf.copy()
return defaultField.copy()
def addField(self, field):
self.deck.modSchema()
@ -70,20 +90,17 @@ insert or replace into models values (?, ?, ?, ?, ?, ?)""",
return dict([(f['name'], (c, f)) for c, f in enumerate(self.fields)])
def sortField(self):
print "sortField() fixme"
return 0
# Templates
##################################################
def loadTemplates(self):
sql = "select * from templates where mid = ? order by ord"
self.templates = [Template(self.deck, data)
for data in self.deck.db.all(sql, self.id)]
def newTemplate(self):
return defaultTemplate.copy()
def addTemplate(self, template):
self.deck.modSchema()
template.mid = self._getID()
template.ord = len(self.templates)
self.templates.append(template)
# Copying
@ -95,15 +112,8 @@ insert or replace into models values (?, ?, ?, ?, ?, ?)""",
new.id = None
new.name += _(" copy")
new.fields = [f.copy() for f in self.fields]
# get new id
t = new.templates; new.templates = []
new.templates = [t.copy() for t in self.templates]
new.flush()
# then put back
new.templates = t
for t in new.templates:
t.id = None
t.mid = new.id
t._flush()
return new
# CSS generation
@ -118,14 +128,10 @@ insert or replace into models values (?, ?, ?, ?, ?, ?)""",
(f['font'], f['qsize'], f['qcol'], f['rtl'], f['pre']))
for c, f in enumerate(self.fields)])
# templates
for t in self.templates:
if not t.id:
# not flushed yet, ignore for now
continue
css += "#cm%s {text-align:%s;background:%s}\n" % (
hexifyID(t.id),
("center", "left", "right")[t.conf['align']],
t.conf['bg'])
css += "".join(["#cm%s-%s {text-align:%s;background:%s}\n" % (
hexifyID(self.id), hexifyID(c),
("center", "left", "right")[t['align']], t['bg'])
for c, t in enumerate(self.templates)])
return css
def _rewriteFont(self, font):
@ -148,60 +154,3 @@ insert or replace into models values (?, ?, ?, ?, ?, ?)""",
t += "white-space:pre-wrap;"
t = "%s {%s}\n" % (prefix, t)
return t
# Field object
##########################################################################
defaultFieldConf = {
'name': "",
'rtl': False,
'req': False,
'uniq': False,
'font': "Arial",
'qsize': 20,
'esize': 20,
'qcol': "#fff",
'pre': True,
}
# Template object
##########################################################################
defaultTemplateConf = {
'hideQ': False,
'align': 0,
'bg': "#000",
'allowEmptyAns': None,
'typeAnswer': None,
'gid': None
}
class Template(object):
def __init__(self, deck, data=None):
self.deck = deck
if data:
self.initFromData(data)
else:
self.id = None
self.active = True
self.conf = defaultTemplateConf.copy()
def initFromData(self, data):
(self.id,
self.mid,
self.ord,
self.name,
self.active,
self.qfmt,
self.afmt,
self.conf) = data
self.conf = simplejson.loads(self.conf)
def _flush(self):
ret = self.deck.db.execute("""
insert or replace into templates values (?, ?, ?, ?, ?, ?, ?, ?)""",
self.id, self.mid, self.ord, self.name,
self.active, self.qfmt, self.afmt,
simplejson.dumps(self.conf))
self.id = ret.lastrowid

View file

@ -2,7 +2,7 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
from anki.models import Model, Template
from anki.models import Model
from anki.lang import _
models = []
@ -21,35 +21,17 @@ def BasicModel(deck):
fm = m.newField()
fm['name'] = _("Back")
m.addField(fm)
t = Template(deck)
t.name = _("Forward")
t.qfmt = "{{" + _("Front") + "}}"
t.afmt = "{{" + _("Back") + "}}"
t = m.newTemplate()
t['name'] = _("Forward")
t['qfmt'] = "{{" + _("Front") + "}}"
t['afmt'] = "{{" + _("Back") + "}}"
m.addTemplate(t)
t = Template(deck)
t.name = _("Reverse")
t.qfmt = "{{" + _("Back") + "}}"
t.afmt = "{{" + _("Front") + "}}"
t.active = False
t = m.newTemplate()
t['name'] = _("Reverse")
t['qfmt'] = "{{" + _("Back") + "}}"
t['afmt'] = "{{" + _("Front") + "}}"
t['actv'] = False
m.addTemplate(t)
return m
models.append(BasicModel)
# Recovery
##########################################################################
def RecoveryModel():
m.name = _("Recovery")
fm = Field(deck)
fm.name = _("Question")
m.addField(fm)
fm = Field(deck)
fm.name = _("Back")
m.addField(fm)
t = Template(deck)
t.name = _("Forward")
t.qfmt = "{{" + _("Question") + "}}"
t.afmt = "{{" + _("Back") + "}}"
m.addTemplate(t)
return m

View file

@ -68,8 +68,8 @@ create table if not exists deck (
create table if not exists cards (
id integer primary key,
fid integer not null,
tid integer not null,
gid integer not null,
ord integer not null,
crt integer not null,
mod integer not null,
type integer not null,
@ -107,21 +107,11 @@ create table if not exists models (
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 templates (
id integer primary key,
mid integer not null,
ord integer not null,
name text not null,
actv integer not null,
qfmt text not null,
afmt text not null,
conf text not null
);
create table if not exists gconf (
id integer primary key,
mod integer not null,
@ -320,7 +310,7 @@ originalPath from media2""")
_moveTable(db, "models")
db.execute("""
insert into models select id, cast(modified as int),
name, "{}", "{}", "" from models2""")
name, "{}", "{}", "{}", "" from models2""")
db.execute("drop table models2")
# reviewHistory -> revlog
@ -390,7 +380,7 @@ utcOffset, "", "", "" from decks""", t=intTime())
def _migrateFieldsTbl(db):
import anki.models
dconf = anki.models.defaultFieldConf
dconf = anki.models.defaultField
mods = {}
for row in db.all("""
select id, modelId, ordinal, name, features, required, "unique",
@ -420,47 +410,55 @@ quizFontFamily, quizFontSize, quizFontColour, editFontSize from fieldModels"""):
db.execute("drop table fieldModels")
return mods
def _migrateTemplatesTbl(db, mods):
def _migrateTemplatesTbl(db, fmods):
import anki.models
db.execute("""
insert into templates select id, modelId, ordinal, name, active, qformat,
aformat, '' from cardModels""")
dconf = anki.models.defaultTemplateConf
dconf = anki.models.defaultTemplate
mods = {}
for row in db.all("""
select id, modelId, questionInAnswer, questionAlign, lastFontColour,
allowEmptyAnswer, typeAnswer from cardModels"""):
select modelId, ordinal, name, active, qformat, aformat, questionInAnswer,
questionAlign, lastFontColour, allowEmptyAnswer, typeAnswer from cardModels"""):
conf = dconf.copy()
(conf['hideQ'],
if row[1] not in mods:
mods[row[0]] = []
(conf['name'],
conf['actv'],
conf['qfmt'],
conf['afmt'],
conf['hideQ'],
conf['align'],
conf['bg'],
conf['allowEmptyAns'],
fname) = row[2:]
conf['emptyAns'],
conf['typeAns']) = row[2:]
# convert the field name to an ordinal
for (ord, fm) in mods[row[1]]:
ordN = None
for (ord, fm) in fmods[row[0]]:
if fm['name'] == row[1]:
conf['typeAnswer'] = ord
ordN = ord
break
# save
db.execute("update templates set conf = ? where id = ?",
simplejson.dumps(conf), row[0])
if ordN is not None:
conf['typeAns'] = ordN
else:
conf['typeAns'] = None
# ensure the new style field format
conf['qfmt'] = re.sub("%\((.+?)\)s", "{{\\1}}", conf['qfmt'])
conf['afmt'] = re.sub("%\((.+?)\)s", "{{\\1}}", conf['afmt'])
# add to model list with ordinal for sorting
mods[row[0]].append((row[1], conf))
# now we've gathered all the info, save it into the models
for mid, tmpls in mods.items():
db.execute("update models set tmpls = ? where id = ?",
simplejson.dumps([x[1] for x in sorted(tmpls)]), mid)
# clean up
db.execute("drop table cardModels")
return mods
def _rewriteModelIds(deck):
# rewrite model/template/field ids
models = deck.allModels()
deck.db.execute("delete from models")
deck.db.execute("delete from templates")
for c, m in enumerate(models):
old = m.id
m.id = c+1
for t in m.templates:
t.mid = m.id
oldT = t.id
t.id = None
t._flush()
deck.db.execute(
"update cards set tid = ? where tid = ?", t.mid, oldT)
m.flush()
deck.db.execute("update facts set mid = ? where mid = ?", m.id, old)
@ -476,12 +474,6 @@ def _postSchemaUpgrade(deck):
"revCardsDue", "revCardsRandom", "acqCardsRandom",
"acqCardsOld", "acqCardsNew"):
deck.db.execute("drop view if exists %s" % v)
# ensure all templates use the new style field format
for m in deck.allModels():
for t in m.templates:
t.qfmt = re.sub("%\((.+?)\)s", "{{\\1}}", t.qfmt)
t.afmt = re.sub("%\((.+?)\)s", "{{\\1}}", t.afmt)
m.flush()
# remove stats, as it's all in the revlog now
deck.db.execute("drop table if exists stats")
# suspended cards don't use ranges anymore

View file

@ -52,7 +52,7 @@ def test_factAddDelete():
f = deck.newFact()
f['Front'] = u"one"; f['Back'] = u"two"
m = f.model
m.templates[1].active = True
m.templates[1]['actv'] = True
m.flush()
n = deck.addFact(f)
assert n == 2

View file

@ -74,7 +74,7 @@ def test_db():
# modify template & regenerate
assert deck.db.scalar("select count() from media") == 1
m = deck.currentModel()
m.templates[0].afmt=u'<img src="{{{Back}}}">'
m.templates[0]['afmt']=u'<img src="{{{Back}}}">'
m.flush()
deck.renderQA(type="all")
assert deck.db.scalar("select count() from media") == 2

View file

@ -1,7 +1,7 @@
# coding: utf-8
from tests.shared import getEmptyDeck
from anki.models import Model, Template
from anki.models import Model
from anki.utils import stripHTML
def test_modelDelete():
@ -20,7 +20,6 @@ def test_modelCopy():
m2 = m.copy()
assert m2.name == "Basic copy"
assert m2.id != m.id
assert m2.templates[0].id != m.templates[0].id
assert len(m2.fields) == 2
assert len(m.fields) == 2
assert len(m2.fields) == len(m.fields)

View file

@ -32,7 +32,7 @@ def test_new():
# the default order should ensure siblings are not seen together, and
# should show all cards
m = d.currentModel()
m.templates[1].active = True
m.templates[1]['actv'] = True
m.flush()
f = d.newFact()
f['Front'] = u"2"; f['Back'] = u"2"