remove the concept of non-active templates

The old template handling was too complicated, and generated frequent
questions on the forums. By dropping non-active templates we can do away with
the generate cards function, and advanced users can simulate the old behaviour
by using conditional field templates.
This commit is contained in:
Damien Elmes 2011-11-08 18:06:19 +09:00
parent f8eefe5ee1
commit 795cdd7d3f
13 changed files with 89 additions and 75 deletions

View file

@ -268,20 +268,19 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
# Card creation # Card creation
########################################################################## ##########################################################################
def findTemplates(self, fact, checkActive=True): def findTemplates(self, fact):
"Return (active), non-empty templates." "Return (active), non-empty templates."
ok = [] ok = []
model = fact.model() model = fact.model()
avail = self.models.availOrds(model, joinFields(fact.fields)) avail = self.models.availOrds(model, joinFields(fact.fields))
ok = [] ok = []
for t in model['tmpls']: for t in model['tmpls']:
if t['actv'] or not checkActive:
if t['ord'] in avail: if t['ord'] in avail:
ok.append(t) ok.append(t)
return ok return ok
def genCards(self, fids, limit=None): def genCards(self, fids):
"Generate cards for active or limited, non-empty templates." "Generate cards for non-empty templates."
# build map of (fid,ord) so we don't create dupes # build map of (fid,ord) so we don't create dupes
sfids = ids2str(fids) sfids = ids2str(fids)
have = {} have = {}
@ -297,11 +296,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
avail = self.models.availOrds(model, flds) avail = self.models.availOrds(model, flds)
ok = [] ok = []
for t in model['tmpls']: for t in model['tmpls']:
if not limit and not t['actv']: if (fid,t['ord']) in have:
continue
elif limit and t not in limit:
continue
elif (fid,t['ord']) in have:
continue continue
if t['ord'] in avail: if t['ord'] in avail:
data.append((ts, fid, t['gid'] or gid, t['ord'], data.append((ts, fid, t['gid'] or gid, t['ord'],
@ -312,13 +307,12 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
insert into cards values (?,?,?,?,?,-1,0,0,?,0,0,0,0,0,0,0,"")""", insert into cards values (?,?,?,?,?,-1,0,0,?,0,0,0,0,0,0,0,"")""",
data) data)
# type 0 - when previewing in add dialog, only non-empty & active # type 0 - when previewing in add dialog, only non-empty
# type 1 - when previewing edit, only existing # type 1 - when previewing edit, only existing
# type 2 - when previewing in models dialog, all # type 2 - when previewing in models dialog, all templates
def previewCards(self, fact, type=0): def previewCards(self, fact, type=0):
"Return uncommited cards for preview."
if type == 0: if type == 0:
cms = self.findTemplates(fact, checkActive=True) cms = self.findTemplates(fact)
elif type == 1: elif type == 1:
cms = [c.template() for c in fact.cards()] cms = [c.template() for c in fact.cards()]
else: else:

View file

@ -84,7 +84,7 @@ If the same name exists, compare checksums."""
# String manipulation # String manipulation
########################################################################## ##########################################################################
def files(self, mid, string, includeRemote=False): def filesInStr(self, mid, string, includeRemote=False):
l = [] l = []
# convert latex first # convert latex first
model = self.deck.models.get(mid) model = self.deck.models.get(mid)
@ -161,7 +161,7 @@ If the same name exists, compare checksums."""
"Return a set of all referenced filenames." "Return a set of all referenced filenames."
files = set() files = set()
for mid, flds in self.deck.db.execute("select mid, flds from facts"): for mid, flds in self.deck.db.execute("select mid, flds from facts"):
for f in self.files(mid, flds): for f in self.filesInStr(mid, flds):
files.add(f) files.add(f)
return files return files

View file

@ -51,7 +51,6 @@ defaultField = {
defaultTemplate = { defaultTemplate = {
'name': "", 'name': "",
'ord': None, 'ord': None,
'actv': True,
'qfmt': "", 'qfmt': "",
'afmt': "", 'afmt': "",
'hideQ': False, 'hideQ': False,
@ -74,13 +73,15 @@ class ModelManager(object):
self.changed = False self.changed = False
self.models = simplejson.loads(json) self.models = simplejson.loads(json)
def save(self, m=None): def save(self, m=None, gencards=False):
"Mark M modified if provided, and schedule registry flush." "Mark M modified if provided, and schedule registry flush."
if m: if m:
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._updateRequired(m)
if gencards:
self.deck.genCards(self.fids(m))
self.changed = True self.changed = True
def flush(self): def flush(self):

View file

@ -22,11 +22,6 @@ def addBasicModel(deck):
t['qfmt'] = "{{" + _("Front") + "}}" t['qfmt'] = "{{" + _("Front") + "}}"
t['afmt'] = "{{" + _("Back") + "}}" t['afmt'] = "{{" + _("Back") + "}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
t = mm.newTemplate(_("Reverse"))
t['qfmt'] = "{{" + _("Back") + "}}"
t['afmt'] = "{{" + _("Front") + "}}"
t['actv'] = False
mm.addTemplate(m, t)
mm.save(m) mm.save(m)
return m return m

View file

@ -379,13 +379,12 @@ order by ordinal""", mid)):
dconf = anki.models.defaultTemplate dconf = anki.models.defaultTemplate
tmpls = [] tmpls = []
for c, row in enumerate(db.all(""" for c, row in enumerate(db.all("""
select name, active, qformat, aformat, questionInAnswer, select name, qformat, aformat, questionInAnswer,
questionAlign, lastFontColour, 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()
(conf['name'], (conf['name'],
conf['actv'],
conf['qfmt'], conf['qfmt'],
conf['afmt'], conf['afmt'],
conf['hideQ'], conf['hideQ'],

View file

@ -6,15 +6,6 @@ from anki.consts import *
from anki.utils import hexifyID from anki.utils import hexifyID
from tests.shared import getEmptyDeck from tests.shared import getEmptyDeck
def test_genCards():
deck = getEmptyDeck()
f = deck.newFact()
f['Front'] = u'1'
f['Back'] = u'2'
deck.addFact(f)
deck.genCards([f.id], f.model()['tmpls'])
assert deck.cardCount() == 2
def test_previewCards(): def test_previewCards():
deck = getEmptyDeck() deck = getEmptyDeck()
f = deck.newFact() f = deck.newFact()
@ -26,7 +17,7 @@ def test_previewCards():
assert cards[0].ord == 0 assert cards[0].ord == 0
# all templates # all templates
cards = deck.previewCards(f, 2) cards = deck.previewCards(f, 2)
assert len(cards) == 2 assert len(cards) == 1
# add the fact, and test existing preview # add the fact, and test existing preview
deck.addFact(f) deck.addFact(f)
cards = deck.previewCards(f, 1) cards = deck.previewCards(f, 1)

View file

@ -48,19 +48,28 @@ def test_factAddDelete():
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"one"; f['Back'] = u"two"
n = deck.addFact(f) n = deck.addFact(f)
assert n == 1 assert n == 1
deck.rollback() # test multiple cards - add another template
assert deck.cardCount() == 0 m = deck.models.current(); mm = deck.models
# try with two cards t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
# the default save doesn't generate cards
assert deck.cardCount() == 1
# but when templates are edited such as in the card layout screen, it
# should generate cards on close
mm.save(m, gencards=True)
assert deck.cardCount() == 2
# creating new facts should use both cards
f = deck.newFact() f = deck.newFact()
f['Front'] = u"one"; f['Back'] = u"two" f['Front'] = u"three"; f['Back'] = u"four"
m = f.model()
m['tmpls'][1]['actv'] = True
deck.models.save(m)
n = deck.addFact(f) n = deck.addFact(f)
assert n == 2 assert n == 2
assert deck.cardCount() == 4
# check q/a generation # check q/a generation
c0 = f.cards()[0] c0 = f.cards()[0]
assert re.sub("</?.+?>", "", c0.q()) == u"one" assert re.sub("</?.+?>", "", c0.q()) == u"three"
# it should not be a duplicate # it should not be a duplicate
for p in f.problems(): for p in f.problems():
assert not p assert not p
@ -74,15 +83,15 @@ def test_factAddDelete():
# try delete the first card # try delete the first card
cards = f.cards() cards = f.cards()
id1 = cards[0].id; id2 = cards[1].id id1 = cards[0].id; id2 = cards[1].id
assert deck.cardCount() == 2 assert deck.cardCount() == 4
assert deck.factCount() == 1 assert deck.factCount() == 2
deck.remCards([id1]) deck.remCards([id1])
assert deck.cardCount() == 1 assert deck.cardCount() == 3
assert deck.factCount() == 1 assert deck.factCount() == 2
# and the second should clear the fact # and the second should clear the fact
deck.remCards([id2]) deck.remCards([id2])
assert deck.cardCount() == 0 assert deck.cardCount() == 2
assert deck.factCount() == 0 assert deck.factCount() == 1
def test_fieldChecksum(): def test_fieldChecksum():
deck = getEmptyDeck() deck = getEmptyDeck()

View file

@ -25,7 +25,12 @@ def test_findCards():
f = deck.newFact() f = deck.newFact()
f['Front'] = u'template test' f['Front'] = u'template test'
f['Back'] = u'foo bar' f['Back'] = u'foo bar'
f.model()['tmpls'][1]['actv'] = True m = deck.models.current(); mm = deck.models
t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
deck.addFact(f) deck.addFact(f)
latestCardIds = [c.id for c in f.cards()] latestCardIds = [c.id for c in f.cards()]
# tag searches # tag searches

View file

@ -21,7 +21,8 @@ def test_latex():
assert "executing latex" in msg assert "executing latex" in msg
assert "installed" in msg assert "installed" in msg
# check if we have latex installed, and abort test if we don't # check if we have latex installed, and abort test if we don't
if not os.path.exists("/usr/bin/latex"): if (not os.path.exists("/usr/bin/latex") and
not os.path.exists("/usr/texbin/latex")):
print "aborting test; latex is not installed" print "aborting test; latex is not installed"
return return
# fix path # fix path
@ -30,10 +31,11 @@ def test_latex():
d.media.check() d.media.check()
assert len(os.listdir(d.media.dir())) == 1 assert len(os.listdir(d.media.dir())) == 1
assert ".png" in f.cards()[0].q() assert ".png" in f.cards()[0].q()
# adding new facts should cause immediate generation # adding new facts should cause generation on question display
f = d.newFact() f = d.newFact()
f['Front'] = u"[latex]world[/latex]" f['Front'] = u"[latex]world[/latex]"
d.addFact(f) d.addFact(f)
f.cards()[0].q()
assert len(os.listdir(d.media.dir())) == 2 assert len(os.listdir(d.media.dir())) == 2
# another fact with the same media should reuse # another fact with the same media should reuse
f = d.newFact() f = d.newFact()

View file

@ -21,7 +21,7 @@ def test_add():
def test_strings(): def test_strings():
d = getEmptyDeck() d = getEmptyDeck()
mf = d.media.files mf = d.media.filesInStr
mid = d.models.models.keys()[0] mid = d.models.models.keys()[0]
assert mf(mid, "aoeu") == [] assert mf(mid, "aoeu") == []
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"] assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]

View file

@ -22,8 +22,8 @@ def test_modelCopy():
assert len(m2['flds']) == 2 assert len(m2['flds']) == 2
assert len(m['flds']) == 2 assert len(m['flds']) == 2
assert len(m2['flds']) == len(m['flds']) assert len(m2['flds']) == len(m['flds'])
assert len(m['tmpls']) == 2 assert len(m['tmpls']) == 1
assert len(m2['tmpls']) == 2 assert len(m2['tmpls']) == 1
assert deck.models.scmhash(m) == deck.models.scmhash(m2) assert deck.models.scmhash(m) == deck.models.scmhash(m2)
def test_fields(): def test_fields():
@ -75,9 +75,12 @@ def test_fields():
def test_templates(): def test_templates():
d = getEmptyDeck() d = getEmptyDeck()
m = d.models.current() m = d.models.current(); mm = d.models
m['tmpls'][1]['actv'] = True t = mm.newTemplate("Reverse")
d.models.save(m) t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = d.newFact() f = d.newFact()
f['Front'] = u'1' f['Front'] = u'1'
f['Back'] = u'2' f['Back'] = u'2'
@ -177,8 +180,12 @@ def test_modelChange():
basic = deck.models.byName("Basic") basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze") cloze = deck.models.byName("Cloze")
# enable second template and add a fact # enable second template and add a fact
basic['tmpls'][1]['actv'] = True m = deck.models.current(); mm = deck.models
deck.models.save(basic) t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = deck.newFact() f = deck.newFact()
f['Front'] = u'f' f['Front'] = u'f'
f['Back'] = u'b' f['Back'] = u'b'

View file

@ -27,7 +27,7 @@ import anki.sync
anki.sync.SYNC_URL = "http://localhost:6543/sync/" anki.sync.SYNC_URL = "http://localhost:6543/sync/"
TEST_USER = "synctest@ichi2.net" TEST_USER = "synctest@ichi2.net"
TEST_PASS = "synctest" TEST_PASS = "synctest"
TEST_HKEY = "k14LvSaEtXFITCJz" TEST_HKEY = "tG5CD9eZbWOru3Yw"
TEST_REMOTE = True TEST_REMOTE = True
def setup_remote(): def setup_remote():
@ -42,7 +42,7 @@ def test_meta():
global TEST_REMOTE global TEST_REMOTE
try: try:
(mod, scm, usn, tstamp, dummy) = ts.server.meta() (mod, scm, usn, tstamp, dummy) = ts.server.meta()
except SyntaxError, e: except Exception, e:
if e.errno == 61: if e.errno == 61:
TEST_REMOTE = False TEST_REMOTE = False
print "aborting; server offline" print "aborting; server offline"
@ -107,6 +107,8 @@ def setup_remoteMedia():
@nose.with_setup(setup_remoteMedia) @nose.with_setup(setup_remoteMedia)
def test_media(): def test_media():
if not TEST_REMOTE:
return
ts.server.mediatest("reset") ts.server.mediatest("reset")
assert len(os.listdir(ts.deck1.media.dir())) == 0 assert len(os.listdir(ts.deck1.media.dir())) == 0
assert ts.server.mediatest("count") == 0 assert ts.server.mediatest("count") == 0

View file

@ -32,9 +32,12 @@ def test_new():
assert c.due >= t assert c.due >= t
# 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.models.current() m = d.models.current(); mm = d.models
m['tmpls'][1]['actv'] = True t = mm.newTemplate("Reverse")
d.models.save(m) t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = d.newFact() f = d.newFact()
f['Front'] = u"2"; f['Back'] = u"2" f['Front'] = u"2"; f['Back'] = u"2"
d.addFact(f) d.addFact(f)
@ -517,8 +520,12 @@ def test_cramLimits():
def test_adjIvl(): def test_adjIvl():
d = getEmptyDeck() d = getEmptyDeck()
# add two more templates and set second active # add two more templates and set second active
m = d.models.current() m = d.models.current(); mm = d.models
m['tmpls'][1]['actv'] = True t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
t = d.models.newTemplate(m) t = d.models.newTemplate(m)
t['name'] = "f2" t['name'] = "f2"
t['qfmt'] = "{{Front}}" t['qfmt'] = "{{Front}}"
@ -582,15 +589,17 @@ def test_adjIvl():
def test_ordcycle(): def test_ordcycle():
d = getEmptyDeck() d = getEmptyDeck()
# add two more templates and set second active # add two more templates and set second active
m = d.models.current() m = d.models.current(); mm = d.models
m['tmpls'][1]['actv'] = True t = mm.newTemplate("Reverse")
t = d.models.newTemplate(m) t['qfmt'] = "{{Back}}"
t['name'] = "f2" t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}" t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}" t['afmt'] = "{{Back}}"
d.models.addTemplate(m, t) mm.addTemplate(m, t)
d.models.save(m) mm.save(m)
# create a new fact; it should have 4 cards # create a new fact; it should have 3 cards
f = d.newFact() f = d.newFact()
f['Front'] = "1"; f['Back'] = "1" f['Front'] = "1"; f['Back'] = "1"
d.addFact(f) d.addFact(f)