From 93dcfceffeaa2c46643d4d860ad015300f1bc8e3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 12 Mar 2011 03:47:15 +0900 Subject: [PATCH] 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 --- anki/cards.py | 6 +- anki/deck.py | 45 +++++++-------- anki/models.py | 129 +++++++++++++------------------------------ anki/stdmodels.py | 38 ++++--------- anki/storage.py | 78 ++++++++++++-------------- tests/test_deck.py | 2 +- tests/test_media.py | 2 +- tests/test_models.py | 3 +- tests/test_sched.py | 2 +- 9 files changed, 112 insertions(+), 193 deletions(-) diff --git a/anki/cards.py b/anki/cards.py index 730fe4e32..da7020a6d 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -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, diff --git a/anki/deck.py b/anki/deck.py index e997ef81d..a95e75ac1 100644 --- a/anki/deck.py +++ b/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 diff --git a/anki/models.py b/anki/models.py index 3e53a3bfd..606956176 100644 --- a/anki/models.py +++ b/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 diff --git a/anki/stdmodels.py b/anki/stdmodels.py index 74317e968..80092506f 100644 --- a/anki/stdmodels.py +++ b/anki/stdmodels.py @@ -2,7 +2,7 @@ # Copyright: Damien Elmes # 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 diff --git a/anki/storage.py b/anki/storage.py index 54045939f..ec8b05536 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -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 diff --git a/tests/test_deck.py b/tests/test_deck.py index 43685bcd2..3abf6a10c 100644 --- a/tests/test_deck.py +++ b/tests/test_deck.py @@ -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 diff --git a/tests/test_media.py b/tests/test_media.py index 1687acd5d..e9d147f38 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -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'' + m.templates[0]['afmt']=u'' m.flush() deck.renderQA(type="all") assert deck.db.scalar("select count() from media") == 2 diff --git a/tests/test_models.py b/tests/test_models.py index 2167a7f43..8a074eca8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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) diff --git a/tests/test_sched.py b/tests/test_sched.py index cfbbabe09..e05ea0da1 100644 --- a/tests/test_sched.py +++ b/tests/test_sched.py @@ -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"