cache required fields, drop emptyAns

For importing and the deck creation wizard, we need to be able to generate
thousands of cards efficiently. So instead of requiring the creation of a fact
and rendering it, we cache the required fields and cloze references in the
model.

Also, emptyAns is dropped, as people can achieve the same behaviour by adding
the required answer fields as conditional to the question.

Todo: refactor genCards() to work in bulk, handle cloze edits intelligently
(prompt to delete invalid references, create new cards as necessary)
This commit is contained in:
Damien Elmes 2011-10-22 02:28:57 +09:00
parent 8bb26aaf74
commit a4e8eb8b74
3 changed files with 76 additions and 19 deletions

View file

@ -5,7 +5,7 @@
import time, os, random, re, stat, simplejson, datetime, copy, shutil import time, os, random, re, stat, simplejson, datetime, copy, shutil
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.utils import ids2str, hexifyID, checksum, fieldChecksum, stripHTML, \ from anki.utils import ids2str, hexifyID, checksum, fieldChecksum, stripHTML, \
intTime, splitFields intTime, splitFields, joinFields
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 ModelManager from anki.models import ModelManager
@ -272,20 +272,12 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
"Return (active), non-empty templates." "Return (active), non-empty templates."
ok = [] ok = []
model = fact.model() model = fact.model()
for template in model['tmpls']: avail = self.models.availOrds(model, joinFields(fact.fields))
if template['actv'] or not checkActive: ok = []
# [cid, fid, mid, gid, ord, tags, flds] for t in model['tmpls']:
data = [1, 1, model['id'], 1, template['ord'], if t['actv'] or not checkActive:
"", fact.joinedFields()] if t['ord'] in avail:
now = self._renderQA(data) ok.append(t)
data[6] = "\x1f".join([""]*len(fact.fields))
empty = self._renderQA(data)
if now['q'] == empty['q']:
continue
if not template['emptyAns']:
if now['a'] == empty['a']:
continue
ok.append(template)
return ok return ok
def genCards(self, fact, templates): def genCards(self, fact, templates):

View file

@ -2,7 +2,7 @@
# 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, copy import simplejson, copy, re
from anki.utils import intTime, hexifyID, joinFields, splitFields, ids2str, \ from anki.utils import intTime, hexifyID, joinFields, splitFields, ids2str, \
timestampID, fieldChecksum timestampID, fieldChecksum
from anki.lang import _ from anki.lang import _
@ -56,7 +56,6 @@ defaultTemplate = {
'hideQ': False, 'hideQ': False,
'align': 0, 'align': 0,
'bg': "#fff", 'bg': "#fff",
'emptyAns': True,
'typeAns': None, 'typeAns': None,
'gid': None, 'gid': None,
} }
@ -80,6 +79,7 @@ class ModelManager(object):
m['mod'] = intTime() m['mod'] = intTime()
m['usn'] = self.deck.usn() m['usn'] = self.deck.usn()
m['css'] = self._css(m) m['css'] = self._css(m)
self._updateRequired(m)
self.changed = True self.changed = True
def flush(self): def flush(self):
@ -430,3 +430,69 @@ select id from facts where mid = ?)""" % " ".join(map),
for t in m['tmpls']: for t in m['tmpls']:
s += t['name'] s += t['name']
return fieldChecksum(s) return fieldChecksum(s)
# Required field/text cache
##########################################################################
def _updateRequired(self, m):
req = []
flds = [f['name'] for f in m['flds']]
for t in m['tmpls']:
ret = self._reqForTemplate(m, flds, t)
req.append((t['ord'], ret[0], ret[1]))
m['req'] = req
def _reqForTemplate(self, m, flds, t):
a = []
b = []
cloze = "cloze" in t['qfmt']
reqstrs = []
if cloze:
# need a cloze-specific filler
cloze = ""
nums = re.findall("\{\{#cloze:(\d+):", t['qfmt'])
for n in nums:
n = int(n)
cloze += "{{c%d::foo}}" % n
# record that we require a specific string for generation
reqstrs.append("{{c%d::" % n)
for f in flds:
a.append(cloze if cloze else "1")
b.append("")
data = [1, 1, m['id'], 1, t['ord'], "", joinFields(b)]
empty = self.deck._renderQA(data)['q']
start = a
req = []
for i in range(len(flds)):
a = start[:]
a[i] = ""
# blank out this field
data[6] = joinFields(a)
# if the result is same as empty, field is required
if self.deck._renderQA(data)['q'] == empty:
req.append(i)
return req, reqstrs
def availOrds(self, m, flds):
"Given a joined field string, return available template ordinals."
have = {}
for c, f in enumerate(splitFields(flds)):
have[c] = f.strip()
avail = []
for ord, req, reqstrs in m['req']:
ok = True
for f in req:
if not have[f]:
# missing and was required
ok = False
break
if not ok:
continue
for s in reqstrs:
if s not in flds:
# required cloze string was missing
ok = False
break
if ok:
avail.append(ord)
return avail

View file

@ -375,7 +375,7 @@ order by ordinal""", mid)):
tmpls = [] tmpls = []
for c, row in enumerate(db.all(""" for c, row in enumerate(db.all("""
select name, active, qformat, aformat, questionInAnswer, select name, active, qformat, aformat, questionInAnswer,
questionAlign, lastFontColour, allowEmptyAnswer, typeAnswer from cardModels questionAlign, lastFontColour, typeAnswer from cardModels
where modelId = ? where modelId = ?
order by ordinal""", mid)): order by ordinal""", mid)):
conf = dconf.copy() conf = dconf.copy()
@ -386,7 +386,6 @@ order by ordinal""", mid)):
conf['hideQ'], conf['hideQ'],
conf['align'], conf['align'],
conf['bg'], conf['bg'],
conf['emptyAns'],
conf['typeAns']) = row conf['typeAns']) = row
conf['ord'] = c conf['ord'] = c
# convert the field name to an ordinal # convert the field name to an ordinal