# coding: utf-8 import time import anki.template from anki.consts import MODEL_CLOZE from anki.utils import isWin, joinFields, stripHTML from tests.shared import getEmptyCol def test_modelDelete(): deck = getEmptyCol() f = deck.newNote() f["Front"] = "1" f["Back"] = "2" deck.addNote(f) assert deck.cardCount() == 1 deck.models.rem(deck.models.current()) assert deck.cardCount() == 0 def test_modelCopy(): deck = getEmptyCol() 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(): d = getEmptyCol() f = d.newNote() f["Front"] = "1" f["Back"] = "2" 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 f = d.models.newField("foo") 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 f = d.models.newField("baz") 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(): d = getEmptyCol() 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() f["Front"] = "1" f["Back"] = "2" 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 t = mm.newTemplate("template name") mm.addTemplate(m, t) assert not d.models.remTemplate(m, m["tmpls"][0]) def test_cloze_ordinals(): d = getEmptyCol() 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"] = "{{text:cloze:Text}}" t["afmt"] = "{{text:cloze:Text}}" mm.addTemplate(m, t) mm.save(m) d.models.remTemplate(m, m["tmpls"][0]) f = d.newNote() f["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}" 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 def test_text(): d = getEmptyCol() m = d.models.current() m["tmpls"][0]["qfmt"] = "{{text:Front}}" d.models.save(m) f = d.newNote() f["Front"] = "helloworld" d.addNote(f) assert "helloworld" in f.cards()[0].q() def test_cloze(): d = getEmptyCol() d.models.setCurrent(d.models.byName("Cloze")) f = d.newNote() assert f.model()["name"] == "Cloze" # a cloze model with no clozes is not empty f["Text"] = "nothing" assert d.addNote(f) # try with one cloze f = d.newNote() f["Text"] = "hello {{c1::world}}" assert d.addNote(f) == 1 assert "hello [...]" in f.cards()[0].q() assert "hello world" in f.cards()[0].a() # and with a comment f = d.newNote() f["Text"] = "hello {{c1::world::typical}}" assert d.addNote(f) == 1 assert "[typical]" in f.cards()[0].q() assert "world" 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 "[...] bar" in c1.q() assert "world bar" in c1.a() assert "world [...]" in c2.q() assert "world bar" 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 "b c" 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 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() f = d.newNote() f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]" assert d.addNote(f) assert len(f.cards()) == 1 assert f.cards()[0].q().endswith(r"\(a\) [...] \[ [...] \]") def test_chained_mods(): d = getEmptyCol() 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}}" t["afmt"] = "{{cloze:text:Text}}" mm.addTemplate(m, t) mm.save(m) d.models.remTemplate(m, m["tmpls"][0]) f = d.newNote() q1 = 'phrase' a1 = "sentence" q2 = 'en chaine' a2 = "chained" f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % ( q1, a1, q2, a2, ) assert d.addNote(f) == 1 assert ( "This [sentence] demonstrates [chained] clozes." in f.cards()[0].q() ) assert ( "This phrase demonstrates en chaine clozes." in f.cards()[0].a() ) def test_modelChange(): deck = getEmptyCol() 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() f["Front"] = "f" f["Back"] = "b123" 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} if isWin: # The low precision timer on Windows reveals a race condition time.sleep(0.05) 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() f["Front"] = "f2" f["Back"] = "b2" 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 map = {0: 0} deck.models.change(cloze, [f.id], basic, map, map) assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1 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) == "" def test_availOrds(): d = getEmptyCol() 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] 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) r = basic["req"][0] assert r[0] == 0 assert r[1] in ("any", "all") assert r[2] == [0] opt = mm.byName("Basic (optional reversed card)") reqSize(opt) r = opt["req"][0] assert r[1] in ("any", "all") assert r[2] == [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", []] opt = mm.byName("Basic (type in the answer)") reqSize(opt) r = opt["req"][0] assert r[1] in ("any", "all") assert r[2] == [0] # 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)