2012-12-21 08:51:59 +01:00
|
|
|
# coding: utf-8
|
|
|
|
|
2014-06-03 10:38:47 +02:00
|
|
|
from tests.shared import getEmptyCol
|
2019-12-15 19:40:38 +01:00
|
|
|
from anki.consts import MODEL_CLOZE
|
2012-12-21 08:51:59 +01:00
|
|
|
from anki.utils import stripHTML, joinFields
|
2016-06-30 14:23:31 +02:00
|
|
|
import anki.template
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def test_modelDelete():
|
2014-06-03 10:38:47 +02:00
|
|
|
deck = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
f = deck.newNote()
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Front'] = '1'
|
|
|
|
f['Back'] = '2'
|
2012-12-21 08:51:59 +01:00
|
|
|
deck.addNote(f)
|
|
|
|
assert deck.cardCount() == 1
|
|
|
|
deck.models.rem(deck.models.current())
|
|
|
|
assert deck.cardCount() == 0
|
|
|
|
|
|
|
|
def test_modelCopy():
|
2014-06-03 10:38:47 +02:00
|
|
|
deck = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
m = deck.models.current()
|
|
|
|
m2 = deck.models.copy(m)
|
|
|
|
assert m2['name'] == "Basic copy"
|
|
|
|
assert m2['id'] != m['id']
|
|
|
|
assert len(m2['flds']) == 2
|
|
|
|
assert len(m['flds']) == 2
|
|
|
|
assert len(m2['flds']) == len(m['flds'])
|
|
|
|
assert len(m['tmpls']) == 1
|
|
|
|
assert len(m2['tmpls']) == 1
|
|
|
|
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
|
|
|
|
|
|
|
|
def test_fields():
|
2014-06-03 10:38:47 +02:00
|
|
|
d = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
f = d.newNote()
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Front'] = '1'
|
|
|
|
f['Back'] = '2'
|
2012-12-21 08:51:59 +01:00
|
|
|
d.addNote(f)
|
|
|
|
m = d.models.current()
|
|
|
|
# make sure renaming a field updates the templates
|
|
|
|
d.models.renameField(m, m['flds'][0], "NewFront")
|
|
|
|
assert "{{NewFront}}" in m['tmpls'][0]['qfmt']
|
|
|
|
h = d.models.scmhash(m)
|
|
|
|
# add a field
|
2019-11-25 00:15:20 +01:00
|
|
|
f = d.models.newField("foo")
|
2012-12-21 08:51:59 +01:00
|
|
|
d.models.addField(m, f)
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
|
|
|
|
assert d.models.scmhash(m) != h
|
|
|
|
# rename it
|
|
|
|
d.models.renameField(m, f, "bar")
|
|
|
|
assert d.getNote(d.models.nids(m)[0])['bar'] == ''
|
|
|
|
# delete back
|
|
|
|
d.models.remField(m, m['flds'][1])
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
|
|
|
|
# move 0 -> 1
|
|
|
|
d.models.moveField(m, m['flds'][0], 1)
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"]
|
|
|
|
# move 1 -> 0
|
|
|
|
d.models.moveField(m, m['flds'][1], 0)
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
|
|
|
|
# add another and put in middle
|
2019-11-25 00:15:20 +01:00
|
|
|
f = d.models.newField("baz")
|
2012-12-21 08:51:59 +01:00
|
|
|
d.models.addField(m, f)
|
|
|
|
f = d.getNote(d.models.nids(m)[0])
|
|
|
|
f['baz'] = "2"
|
|
|
|
f.flush()
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
|
|
|
|
# move 2 -> 1
|
|
|
|
d.models.moveField(m, m['flds'][2], 1)
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
|
|
|
|
# move 0 -> 2
|
|
|
|
d.models.moveField(m, m['flds'][0], 2)
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"]
|
|
|
|
# move 0 -> 1
|
|
|
|
d.models.moveField(m, m['flds'][0], 1)
|
|
|
|
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
|
|
|
|
|
|
|
|
def test_templates():
|
2014-06-03 10:38:47 +02:00
|
|
|
d = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
m = d.models.current(); mm = d.models
|
|
|
|
t = mm.newTemplate("Reverse")
|
|
|
|
t['qfmt'] = "{{Back}}"
|
|
|
|
t['afmt'] = "{{Front}}"
|
|
|
|
mm.addTemplate(m, t)
|
|
|
|
mm.save(m)
|
|
|
|
f = d.newNote()
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Front'] = '1'
|
|
|
|
f['Back'] = '2'
|
2012-12-21 08:51:59 +01:00
|
|
|
d.addNote(f)
|
|
|
|
assert d.cardCount() == 2
|
|
|
|
(c, c2) = f.cards()
|
|
|
|
# first card should have first ord
|
|
|
|
assert c.ord == 0
|
|
|
|
assert c2.ord == 1
|
|
|
|
# switch templates
|
|
|
|
d.models.moveTemplate(m, c.template(), 1)
|
|
|
|
c.load(); c2.load()
|
|
|
|
assert c.ord == 1
|
|
|
|
assert c2.ord == 0
|
|
|
|
# removing a template should delete its cards
|
|
|
|
assert d.models.remTemplate(m, m['tmpls'][0])
|
|
|
|
assert d.cardCount() == 1
|
|
|
|
# and should have updated the other cards' ordinals
|
|
|
|
c = f.cards()[0]
|
|
|
|
assert c.ord == 0
|
|
|
|
assert stripHTML(c.q()) == "1"
|
|
|
|
# it shouldn't be possible to orphan notes by removing templates
|
2019-11-27 08:09:30 +01:00
|
|
|
t = mm.newTemplate("template name")
|
2012-12-21 08:51:59 +01:00
|
|
|
mm.addTemplate(m, t)
|
|
|
|
assert not d.models.remTemplate(m, m['tmpls'][0])
|
|
|
|
|
2014-02-18 18:11:31 +01:00
|
|
|
def test_cloze_ordinals():
|
2014-06-03 10:38:47 +02:00
|
|
|
d = getEmptyCol()
|
2014-02-18 18:11:31 +01:00
|
|
|
d.models.setCurrent(d.models.byName("Cloze"))
|
|
|
|
m = d.models.current(); mm = d.models
|
|
|
|
|
|
|
|
#We replace the default Cloze template
|
|
|
|
t = mm.newTemplate("ChainedCloze")
|
2014-06-03 11:05:09 +02:00
|
|
|
t['qfmt'] = "{{text:cloze:Text}}"
|
|
|
|
t['afmt'] = "{{text:cloze:Text}}"
|
2014-02-18 18:11:31 +01:00
|
|
|
mm.addTemplate(m, t)
|
|
|
|
mm.save(m)
|
|
|
|
d.models.remTemplate(m, m['tmpls'][0])
|
|
|
|
|
|
|
|
f = d.newNote()
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
|
2014-02-18 18:11:31 +01:00
|
|
|
d.addNote(f)
|
|
|
|
assert d.cardCount() == 2
|
|
|
|
(c, c2) = f.cards()
|
|
|
|
# first card should have first ord
|
|
|
|
assert c.ord == 0
|
|
|
|
assert c2.ord == 1
|
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def test_text():
|
2014-06-03 10:38:47 +02:00
|
|
|
d = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
m = d.models.current()
|
|
|
|
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
|
|
|
|
d.models.save(m)
|
|
|
|
f = d.newNote()
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Front'] = 'hello<b>world'
|
2012-12-21 08:51:59 +01:00
|
|
|
d.addNote(f)
|
|
|
|
assert "helloworld" in f.cards()[0].q()
|
|
|
|
|
|
|
|
def test_cloze():
|
2014-06-03 10:38:47 +02:00
|
|
|
d = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
d.models.setCurrent(d.models.byName("Cloze"))
|
|
|
|
f = d.newNote()
|
|
|
|
assert f.model()['name'] == "Cloze"
|
|
|
|
# a cloze model with no clozes is not empty
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Text'] = 'nothing'
|
2012-12-21 08:51:59 +01:00
|
|
|
assert d.addNote(f)
|
|
|
|
# try with one cloze
|
|
|
|
f = d.newNote()
|
|
|
|
f['Text'] = "hello {{c1::world}}"
|
|
|
|
assert d.addNote(f) == 1
|
|
|
|
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
|
|
|
|
assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
|
|
|
|
# and with a comment
|
|
|
|
f = d.newNote()
|
|
|
|
f['Text'] = "hello {{c1::world::typical}}"
|
|
|
|
assert d.addNote(f) == 1
|
|
|
|
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
|
|
|
|
assert "<span class=cloze>world</span>" in f.cards()[0].a()
|
|
|
|
# and with 2 clozes
|
|
|
|
f = d.newNote()
|
|
|
|
f['Text'] = "hello {{c1::world}} {{c2::bar}}"
|
|
|
|
assert d.addNote(f) == 2
|
|
|
|
(c1, c2) = f.cards()
|
|
|
|
assert "<span class=cloze>[...]</span> bar" in c1.q()
|
|
|
|
assert "<span class=cloze>world</span> bar" in c1.a()
|
|
|
|
assert "world <span class=cloze>[...]</span>" in c2.q()
|
|
|
|
assert "world <span class=cloze>bar</span>" in c2.a()
|
|
|
|
# if there are multiple answers for a single cloze, they are given in a
|
|
|
|
# list
|
|
|
|
f = d.newNote()
|
|
|
|
f['Text'] = "a {{c1::b}} {{c1::c}}"
|
|
|
|
assert d.addNote(f) == 1
|
|
|
|
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (
|
|
|
|
f.cards()[0].a())
|
|
|
|
# if we add another cloze, a card should be generated
|
|
|
|
cnt = d.cardCount()
|
|
|
|
f['Text'] = "{{c2::hello}} {{c1::foo}}"
|
|
|
|
f.flush()
|
|
|
|
assert d.cardCount() == cnt + 1
|
|
|
|
# 0 or negative indices are not supported
|
|
|
|
f['Text'] += "{{c0::zero}} {{c-1:foo}}"
|
|
|
|
f.flush()
|
|
|
|
assert len(f.cards()) == 2
|
|
|
|
|
2017-09-12 05:53:08 +02:00
|
|
|
def test_cloze_mathjax():
|
|
|
|
d = getEmptyCol()
|
|
|
|
d.models.setCurrent(d.models.byName("Cloze"))
|
|
|
|
f = d.newNote()
|
|
|
|
f['Text'] = r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}'
|
|
|
|
assert d.addNote(f)
|
|
|
|
assert len(f.cards()) == 5
|
|
|
|
assert "class=cloze" in f.cards()[0].q()
|
|
|
|
assert "class=cloze" in f.cards()[1].q()
|
|
|
|
assert "class=cloze" not in f.cards()[2].q()
|
|
|
|
assert "class=cloze" in f.cards()[3].q()
|
|
|
|
assert "class=cloze" in f.cards()[4].q()
|
|
|
|
|
2019-12-22 04:28:29 +01:00
|
|
|
f = d.newNote()
|
|
|
|
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
|
|
|
|
assert d.addNote(f)
|
|
|
|
assert len(f.cards()) == 1
|
2019-12-22 05:47:45 +01:00
|
|
|
assert f.cards()[0].q().endswith('\(a\) <span class=cloze>[...]</span> \[ [...] \]')
|
2019-12-22 04:28:29 +01:00
|
|
|
|
|
|
|
|
2014-02-18 18:11:31 +01:00
|
|
|
def test_chained_mods():
|
2014-06-03 10:38:47 +02:00
|
|
|
d = getEmptyCol()
|
2014-02-18 18:11:31 +01:00
|
|
|
d.models.setCurrent(d.models.byName("Cloze"))
|
|
|
|
m = d.models.current(); mm = d.models
|
|
|
|
|
|
|
|
#We replace the default Cloze template
|
|
|
|
t = mm.newTemplate("ChainedCloze")
|
|
|
|
t['qfmt'] = "{{cloze:text:Text}}"
|
2014-04-15 18:39:09 +02:00
|
|
|
t['afmt'] = "{{cloze:text:Text}}"
|
2014-02-18 18:11:31 +01:00
|
|
|
mm.addTemplate(m, t)
|
|
|
|
mm.save(m)
|
|
|
|
d.models.remTemplate(m, m['tmpls'][0])
|
|
|
|
|
|
|
|
f = d.newNote()
|
|
|
|
q1 = '<span style=\"color:red\">phrase</span>'
|
|
|
|
a1 = '<b>sentence</b>'
|
|
|
|
q2 = '<span style=\"color:red\">en chaine</span>'
|
|
|
|
a2 = '<i>chained</i>'
|
|
|
|
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
|
|
|
|
assert d.addNote(f) == 1
|
|
|
|
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q()
|
|
|
|
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def test_modelChange():
|
2014-06-03 10:38:47 +02:00
|
|
|
deck = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
basic = deck.models.byName("Basic")
|
|
|
|
cloze = deck.models.byName("Cloze")
|
|
|
|
# enable second template and add a note
|
|
|
|
m = deck.models.current(); mm = deck.models
|
|
|
|
t = mm.newTemplate("Reverse")
|
|
|
|
t['qfmt'] = "{{Back}}"
|
|
|
|
t['afmt'] = "{{Front}}"
|
|
|
|
mm.addTemplate(m, t)
|
|
|
|
mm.save(m)
|
|
|
|
f = deck.newNote()
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Front'] = 'f'
|
|
|
|
f['Back'] = 'b123'
|
2012-12-21 08:51:59 +01:00
|
|
|
deck.addNote(f)
|
|
|
|
# switch fields
|
|
|
|
map = {0: 1, 1: 0}
|
|
|
|
deck.models.change(basic, [f.id], basic, map, None)
|
|
|
|
f.load()
|
|
|
|
assert f['Front'] == 'b123'
|
|
|
|
assert f['Back'] == 'f'
|
|
|
|
# switch cards
|
|
|
|
c0 = f.cards()[0]
|
|
|
|
c1 = f.cards()[1]
|
|
|
|
assert "b123" in c0.q()
|
|
|
|
assert "f" in c1.q()
|
|
|
|
assert c0.ord == 0
|
|
|
|
assert c1.ord == 1
|
|
|
|
deck.models.change(basic, [f.id], basic, None, map)
|
|
|
|
f.load(); c0.load(); c1.load()
|
|
|
|
assert "f" in c0.q()
|
|
|
|
assert "b123" in c1.q()
|
|
|
|
assert c0.ord == 1
|
|
|
|
assert c1.ord == 0
|
|
|
|
# .cards() returns cards in order
|
|
|
|
assert f.cards()[0].id == c1.id
|
|
|
|
# delete first card
|
|
|
|
map = {0: None, 1: 1}
|
|
|
|
deck.models.change(basic, [f.id], basic, None, map)
|
|
|
|
f.load()
|
|
|
|
c0.load()
|
|
|
|
# the card was deleted
|
|
|
|
try:
|
|
|
|
c1.load()
|
|
|
|
assert 0
|
|
|
|
except TypeError:
|
|
|
|
pass
|
|
|
|
# but we have two cards, as a new one was generated
|
|
|
|
assert len(f.cards()) == 2
|
|
|
|
# an unmapped field becomes blank
|
|
|
|
assert f['Front'] == 'b123'
|
|
|
|
assert f['Back'] == 'f'
|
|
|
|
deck.models.change(basic, [f.id], basic, map, None)
|
|
|
|
f.load()
|
|
|
|
assert f['Front'] == ''
|
|
|
|
assert f['Back'] == 'f'
|
|
|
|
# another note to try model conversion
|
|
|
|
f = deck.newNote()
|
2016-05-12 06:45:35 +02:00
|
|
|
f['Front'] = 'f2'
|
|
|
|
f['Back'] = 'b2'
|
2012-12-21 08:51:59 +01:00
|
|
|
deck.addNote(f)
|
|
|
|
assert deck.models.useCount(basic) == 2
|
|
|
|
assert deck.models.useCount(cloze) == 0
|
|
|
|
map = {0: 0, 1: 1}
|
|
|
|
deck.models.change(basic, [f.id], cloze, map, map)
|
|
|
|
f.load()
|
|
|
|
assert f['Text'] == "f2"
|
|
|
|
assert len(f.cards()) == 2
|
|
|
|
# back the other way, with deletion of second ord
|
|
|
|
deck.models.remTemplate(basic, basic['tmpls'][1])
|
|
|
|
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 2
|
|
|
|
deck.models.change(cloze, [f.id], basic, map, map)
|
|
|
|
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
|
|
|
|
|
2016-06-30 14:23:31 +02:00
|
|
|
def test_templates():
|
|
|
|
d = dict(Foo="x", Bar="y")
|
|
|
|
assert anki.template.render("{{Foo}}", d) == "x"
|
|
|
|
assert anki.template.render("{{#Foo}}{{Foo}}{{/Foo}}", d) == "x"
|
|
|
|
assert anki.template.render("{{#Foo}}{{Foo}}{{/Foo}}", d) == "x"
|
|
|
|
assert anki.template.render("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x"
|
|
|
|
assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def test_availOrds():
|
2014-06-03 10:38:47 +02:00
|
|
|
d = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
m = d.models.current(); mm = d.models
|
|
|
|
t = m['tmpls'][0]
|
|
|
|
f = d.newNote()
|
|
|
|
f['Front'] = "1"
|
|
|
|
# simple templates
|
|
|
|
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
|
|
|
t['qfmt'] = "{{Back}}"
|
|
|
|
mm.save(m, templates=True)
|
|
|
|
assert not mm.availOrds(m, joinFields(f.fields))
|
|
|
|
# AND
|
|
|
|
t['qfmt'] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
|
|
|
mm.save(m, templates=True)
|
|
|
|
assert not mm.availOrds(m, joinFields(f.fields))
|
|
|
|
t['qfmt'] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
|
|
|
|
mm.save(m, templates=True)
|
|
|
|
assert not mm.availOrds(m, joinFields(f.fields))
|
|
|
|
# OR
|
|
|
|
t['qfmt'] = "{{Front}}\n{{Back}}"
|
|
|
|
mm.save(m, templates=True)
|
|
|
|
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
|
|
|
t['Front'] = ""
|
|
|
|
t['Back'] = "1"
|
|
|
|
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
2019-12-15 19:40:38 +01:00
|
|
|
|
|
|
|
def test_req():
|
|
|
|
def reqSize(model):
|
|
|
|
if model['type'] == MODEL_CLOZE:
|
|
|
|
return
|
|
|
|
assert (len(model['tmpls']) == len(model['req']))
|
|
|
|
|
|
|
|
d = getEmptyCol()
|
|
|
|
mm = d.models
|
|
|
|
basic = mm.byName("Basic")
|
|
|
|
assert 'req' in basic
|
|
|
|
reqSize(basic)
|
|
|
|
assert basic['req'][0] == [0, 'all', [0]]
|
|
|
|
opt = mm.byName("Basic (optional reversed card)")
|
|
|
|
reqSize(opt)
|
|
|
|
assert opt['req'][0] == [0, 'all', [0]]
|
|
|
|
assert opt['req'][1] == [1, 'all', [1, 2]]
|
|
|
|
#testing any
|
|
|
|
opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}"
|
|
|
|
mm.save(opt, templates=True)
|
|
|
|
assert opt['req'][1] == [1, 'any', [1, 2]]
|
|
|
|
#testing None
|
|
|
|
opt['tmpls'][1]['qfmt'] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
|
|
|
|
mm.save(opt, templates=True)
|
|
|
|
assert opt['req'][1] == [1, 'none', []]
|
2019-12-24 01:23:21 +01:00
|
|
|
|
|
|
|
def test_updatereqs_performance():
|
|
|
|
import time
|
|
|
|
d = getEmptyCol()
|
|
|
|
mm = d.models
|
|
|
|
m = mm.byName("Basic")
|
|
|
|
for i in range(100):
|
|
|
|
fld = mm.newField(f"field{i}")
|
|
|
|
mm.addField(m, fld)
|
|
|
|
tmpl = mm.newTemplate(f"template{i}")
|
|
|
|
tmpl['qfmt'] = "{{field%s}}" % i
|
|
|
|
mm.addTemplate(m, tmpl)
|
|
|
|
t = time.time()
|
|
|
|
mm.save(m, templates=True)
|
|
|
|
print("took", (time.time()-t)*100)
|