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

View file

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

View file

@ -12,6 +12,31 @@ from anki.lang import _
defaultConf = { 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): class Model(object):
def __init__(self, deck, id=None): def __init__(self, deck, id=None):
@ -32,34 +57,29 @@ class Model(object):
(self.mod, (self.mod,
self.name, self.name,
self.fields, self.fields,
self.templates,
self.conf) = self.deck.db.first(""" 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.fields = simplejson.loads(self.fields)
self.templates = simplejson.loads(self.templates)
self.conf = simplejson.loads(self.conf) self.conf = simplejson.loads(self.conf)
self.loadTemplates()
def flush(self): def flush(self):
self.mod = intTime() self.mod = intTime()
ret = self.deck.db.execute(""" ret = self.deck.db.execute("""
insert or replace into models values (?, ?, ?, ?, ?, ?)""", insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""",
self.id, self.mod, self.name, self.id, self.mod, self.name,
simplejson.dumps(self.fields), simplejson.dumps(self.fields),
simplejson.dumps(self.templates),
simplejson.dumps(self.conf), simplejson.dumps(self.conf),
self.genCSS()) self.genCSS())
self.id = ret.lastrowid 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 # Fields
################################################## ##################################################
def newField(self): def newField(self):
return defaultFieldConf.copy() return defaultField.copy()
def addField(self, field): def addField(self, field):
self.deck.modSchema() 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)]) return dict([(f['name'], (c, f)) for c, f in enumerate(self.fields)])
def sortField(self): def sortField(self):
print "sortField() fixme"
return 0 return 0
# Templates # Templates
################################################## ##################################################
def loadTemplates(self): def newTemplate(self):
sql = "select * from templates where mid = ? order by ord" return defaultTemplate.copy()
self.templates = [Template(self.deck, data)
for data in self.deck.db.all(sql, self.id)]
def addTemplate(self, template): def addTemplate(self, template):
self.deck.modSchema() self.deck.modSchema()
template.mid = self._getID()
template.ord = len(self.templates)
self.templates.append(template) self.templates.append(template)
# Copying # Copying
@ -95,15 +112,8 @@ insert or replace into models values (?, ?, ?, ?, ?, ?)""",
new.id = None new.id = None
new.name += _(" copy") new.name += _(" copy")
new.fields = [f.copy() for f in self.fields] new.fields = [f.copy() for f in self.fields]
# get new id new.templates = [t.copy() for t in self.templates]
t = new.templates; new.templates = []
new.flush() new.flush()
# then put back
new.templates = t
for t in new.templates:
t.id = None
t.mid = new.id
t._flush()
return new return new
# CSS generation # CSS generation
@ -118,14 +128,10 @@ insert or replace into models values (?, ?, ?, ?, ?, ?)""",
(f['font'], f['qsize'], f['qcol'], f['rtl'], f['pre'])) (f['font'], f['qsize'], f['qcol'], f['rtl'], f['pre']))
for c, f in enumerate(self.fields)]) for c, f in enumerate(self.fields)])
# templates # templates
for t in self.templates: css += "".join(["#cm%s-%s {text-align:%s;background:%s}\n" % (
if not t.id: hexifyID(self.id), hexifyID(c),
# not flushed yet, ignore for now ("center", "left", "right")[t['align']], t['bg'])
continue for c, t in enumerate(self.templates)])
css += "#cm%s {text-align:%s;background:%s}\n" % (
hexifyID(t.id),
("center", "left", "right")[t.conf['align']],
t.conf['bg'])
return css return css
def _rewriteFont(self, font): def _rewriteFont(self, font):
@ -148,60 +154,3 @@ insert or replace into models values (?, ?, ?, ?, ?, ?)""",
t += "white-space:pre-wrap;" t += "white-space:pre-wrap;"
t = "%s {%s}\n" % (prefix, t) t = "%s {%s}\n" % (prefix, t)
return 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> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html # 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 _ from anki.lang import _
models = [] models = []
@ -21,35 +21,17 @@ def BasicModel(deck):
fm = m.newField() fm = m.newField()
fm['name'] = _("Back") fm['name'] = _("Back")
m.addField(fm) m.addField(fm)
t = Template(deck) t = m.newTemplate()
t.name = _("Forward") t['name'] = _("Forward")
t.qfmt = "{{" + _("Front") + "}}" t['qfmt'] = "{{" + _("Front") + "}}"
t.afmt = "{{" + _("Back") + "}}" t['afmt'] = "{{" + _("Back") + "}}"
m.addTemplate(t) m.addTemplate(t)
t = Template(deck) t = m.newTemplate()
t.name = _("Reverse") t['name'] = _("Reverse")
t.qfmt = "{{" + _("Back") + "}}" t['qfmt'] = "{{" + _("Back") + "}}"
t.afmt = "{{" + _("Front") + "}}" t['afmt'] = "{{" + _("Front") + "}}"
t.active = False t['actv'] = False
m.addTemplate(t) m.addTemplate(t)
return m return m
models.append(BasicModel) 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 ( create table if not exists cards (
id integer primary key, id integer primary key,
fid integer not null, fid integer not null,
tid integer not null,
gid integer not null, gid integer not null,
ord integer not null,
crt integer not null, crt integer not null,
mod integer not null, mod integer not null,
type integer not null, type integer not null,
@ -107,21 +107,11 @@ create table if not exists models (
mod integer not null, mod integer not null,
name text not null, name text not null,
flds text not null, flds text not null,
tmpls text not null,
conf text not null, conf text not null,
css 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 ( create table if not exists gconf (
id integer primary key, id integer primary key,
mod integer not null, mod integer not null,
@ -320,7 +310,7 @@ originalPath from media2""")
_moveTable(db, "models") _moveTable(db, "models")
db.execute(""" db.execute("""
insert into models select id, cast(modified as int), insert into models select id, cast(modified as int),
name, "{}", "{}", "" from models2""") name, "{}", "{}", "{}", "" from models2""")
db.execute("drop table models2") db.execute("drop table models2")
# reviewHistory -> revlog # reviewHistory -> revlog
@ -390,7 +380,7 @@ utcOffset, "", "", "" from decks""", t=intTime())
def _migrateFieldsTbl(db): def _migrateFieldsTbl(db):
import anki.models import anki.models
dconf = anki.models.defaultFieldConf dconf = anki.models.defaultField
mods = {} mods = {}
for row in db.all(""" for row in db.all("""
select id, modelId, ordinal, name, features, required, "unique", select id, modelId, ordinal, name, features, required, "unique",
@ -420,47 +410,55 @@ quizFontFamily, quizFontSize, quizFontColour, editFontSize from fieldModels"""):
db.execute("drop table fieldModels") db.execute("drop table fieldModels")
return mods return mods
def _migrateTemplatesTbl(db, mods): def _migrateTemplatesTbl(db, fmods):
import anki.models import anki.models
db.execute(""" dconf = anki.models.defaultTemplate
insert into templates select id, modelId, ordinal, name, active, qformat, mods = {}
aformat, '' from cardModels""")
dconf = anki.models.defaultTemplateConf
for row in db.all(""" for row in db.all("""
select id, modelId, questionInAnswer, questionAlign, lastFontColour, select modelId, ordinal, name, active, qformat, aformat, questionInAnswer,
allowEmptyAnswer, typeAnswer from cardModels"""): questionAlign, lastFontColour, allowEmptyAnswer, typeAnswer from cardModels"""):
conf = dconf.copy() 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['align'],
conf['bg'], conf['bg'],
conf['allowEmptyAns'], conf['emptyAns'],
fname) = row[2:] conf['typeAns']) = row[2:]
# convert the field name to an ordinal # 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]: if fm['name'] == row[1]:
conf['typeAnswer'] = ord ordN = ord
break break
# save if ordN is not None:
db.execute("update templates set conf = ? where id = ?", conf['typeAns'] = ordN
simplejson.dumps(conf), row[0]) 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 # clean up
db.execute("drop table cardModels") db.execute("drop table cardModels")
return mods
def _rewriteModelIds(deck): def _rewriteModelIds(deck):
# rewrite model/template/field ids # rewrite model/template/field ids
models = deck.allModels() models = deck.allModels()
deck.db.execute("delete from models") deck.db.execute("delete from models")
deck.db.execute("delete from templates")
for c, m in enumerate(models): for c, m in enumerate(models):
old = m.id old = m.id
m.id = c+1 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() m.flush()
deck.db.execute("update facts set mid = ? where mid = ?", m.id, old) deck.db.execute("update facts set mid = ? where mid = ?", m.id, old)
@ -476,12 +474,6 @@ def _postSchemaUpgrade(deck):
"revCardsDue", "revCardsRandom", "acqCardsRandom", "revCardsDue", "revCardsRandom", "acqCardsRandom",
"acqCardsOld", "acqCardsNew"): "acqCardsOld", "acqCardsNew"):
deck.db.execute("drop view if exists %s" % v) 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 # remove stats, as it's all in the revlog now
deck.db.execute("drop table if exists stats") deck.db.execute("drop table if exists stats")
# suspended cards don't use ranges anymore # suspended cards don't use ranges anymore

View file

@ -52,7 +52,7 @@ 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].active = True m.templates[1]['actv'] = True
m.flush() m.flush()
n = deck.addFact(f) n = deck.addFact(f)
assert n == 2 assert n == 2

View file

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

View file

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
from tests.shared import getEmptyDeck from tests.shared import getEmptyDeck
from anki.models import Model, Template from anki.models import Model
from anki.utils import stripHTML from anki.utils import stripHTML
def test_modelDelete(): def test_modelDelete():
@ -20,7 +20,6 @@ def test_modelCopy():
m2 = m.copy() m2 = m.copy()
assert m2.name == "Basic copy" assert m2.name == "Basic copy"
assert m2.id != m.id assert m2.id != m.id
assert m2.templates[0].id != m.templates[0].id
assert len(m2.fields) == 2 assert len(m2.fields) == 2
assert len(m.fields) == 2 assert len(m.fields) == 2
assert len(m2.fields) == len(m.fields) 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 # the default order should ensure siblings are not seen together, and
# should show all cards # should show all cards
m = d.currentModel() m = d.currentModel()
m.templates[1].active = True m.templates[1]['actv'] = True
m.flush() m.flush()
f = d.newFact() f = d.newFact()
f['Front'] = u"2"; f['Back'] = u"2" f['Front'] = u"2"; f['Back'] = u"2"