mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
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:
parent
9cec4b2059
commit
93dcfceffe
9 changed files with 112 additions and 193 deletions
|
@ -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,
|
||||
|
|
45
anki/deck.py
45
anki/deck.py
|
@ -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
|
||||
|
|
129
anki/models.py
129
anki/models.py
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue