# coding: utf-8 import time from anki.consts import MODEL_CLOZE from anki.errors import NotFoundError from anki.utils import isWin, stripHTML from tests.shared import getEmptyCol def test_modelDelete(): col = getEmptyCol() note = col.newNote() note["Front"] = "1" note["Back"] = "2" col.addNote(note) assert col.cardCount() == 1 col.models.rem(col.models.current()) assert col.cardCount() == 0 def test_modelCopy(): col = getEmptyCol() m = col.models.current() m2 = col.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 col.models.scmhash(m) == col.models.scmhash(m2) def test_fields(): col = getEmptyCol() note = col.newNote() note["Front"] = "1" note["Back"] = "2" col.addNote(note) m = col.models.current() # make sure renaming a field updates the templates col.models.renameField(m, m["flds"][0], "NewFront") assert "{{NewFront}}" in m["tmpls"][0]["qfmt"] h = col.models.scmhash(m) # add a field field = col.models.newField("foo") col.models.addField(m, field) assert col.get_note(col.models.nids(m)[0]).fields == ["1", "2", ""] assert col.models.scmhash(m) != h # rename it field = m["flds"][2] col.models.renameField(m, field, "bar") assert col.get_note(col.models.nids(m)[0])["bar"] == "" # delete back col.models.remField(m, m["flds"][1]) assert col.get_note(col.models.nids(m)[0]).fields == ["1", ""] # move 0 -> 1 col.models.moveField(m, m["flds"][0], 1) assert col.get_note(col.models.nids(m)[0]).fields == ["", "1"] # move 1 -> 0 col.models.moveField(m, m["flds"][1], 0) assert col.get_note(col.models.nids(m)[0]).fields == ["1", ""] # add another and put in middle field = col.models.newField("baz") col.models.addField(m, field) note = col.get_note(col.models.nids(m)[0]) note["baz"] = "2" note.flush() assert col.get_note(col.models.nids(m)[0]).fields == ["1", "", "2"] # move 2 -> 1 col.models.moveField(m, m["flds"][2], 1) assert col.get_note(col.models.nids(m)[0]).fields == ["1", "2", ""] # move 0 -> 2 col.models.moveField(m, m["flds"][0], 2) assert col.get_note(col.models.nids(m)[0]).fields == ["2", "", "1"] # move 0 -> 1 col.models.moveField(m, m["flds"][0], 1) assert col.get_note(col.models.nids(m)[0]).fields == ["", "2", "1"] def test_templates(): col = getEmptyCol() m = col.models.current() mm = col.models t = mm.newTemplate("Reverse") t["qfmt"] = "{{Back}}" t["afmt"] = "{{Front}}" mm.addTemplate(m, t) mm.save(m) note = col.newNote() note["Front"] = "1" note["Back"] = "2" col.addNote(note) assert col.cardCount() == 2 (c, c2) = note.cards() # first card should have first ord assert c.ord == 0 assert c2.ord == 1 # switch templates col.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 col.models.remTemplate(m, m["tmpls"][0]) assert col.cardCount() == 1 # and should have updated the other cards' ordinals c = note.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) col.models.remTemplate(m, m["tmpls"][0]) assert ( col.db.scalar( "select count() from cards where nid not in (select id from notes)" ) == 0 ) def test_cloze_ordinals(): col = getEmptyCol() col.models.setCurrent(col.models.byName("Cloze")) m = col.models.current() mm = col.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) col.models.remTemplate(m, m["tmpls"][0]) note = col.newNote() note["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}" col.addNote(note) assert col.cardCount() == 2 (c, c2) = note.cards() # first card should have first ord assert c.ord == 0 assert c2.ord == 1 def test_text(): col = getEmptyCol() m = col.models.current() m["tmpls"][0]["qfmt"] = "{{text:Front}}" col.models.save(m) note = col.newNote() note["Front"] = "helloworld" col.addNote(note) assert "helloworld" in note.cards()[0].q() def test_cloze(): col = getEmptyCol() col.models.setCurrent(col.models.byName("Cloze")) note = col.newNote() assert note.model()["name"] == "Cloze" # a cloze model with no clozes is not empty note["Text"] = "nothing" assert col.addNote(note) # try with one cloze note = col.newNote() note["Text"] = "hello {{c1::world}}" assert col.addNote(note) == 1 assert "hello [...]" in note.cards()[0].q() assert "hello world" in note.cards()[0].a() # and with a comment note = col.newNote() note["Text"] = "hello {{c1::world::typical}}" assert col.addNote(note) == 1 assert "[typical]" in note.cards()[0].q() assert "world" in note.cards()[0].a() # and with 2 clozes note = col.newNote() note["Text"] = "hello {{c1::world}} {{c2::bar}}" assert col.addNote(note) == 2 (c1, c2) = note.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 note = col.newNote() note["Text"] = "a {{c1::b}} {{c1::c}}" assert col.addNote(note) == 1 assert "b c" in ( note.cards()[0].a() ) # if we add another cloze, a card should be generated cnt = col.cardCount() note["Text"] = "{{c2::hello}} {{c1::foo}}" note.flush() assert col.cardCount() == cnt + 1 # 0 or negative indices are not supported note["Text"] += "{{c0::zero}} {{c-1:foo}}" note.flush() assert len(note.cards()) == 2 def test_cloze_mathjax(): col = getEmptyCol() col.models.setCurrent(col.models.byName("Cloze")) note = col.newNote() note[ "Text" ] = r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}" assert col.addNote(note) assert len(note.cards()) == 5 assert "class=cloze" in note.cards()[0].q() assert "class=cloze" in note.cards()[1].q() assert "class=cloze" not in note.cards()[2].q() assert "class=cloze" in note.cards()[3].q() assert "class=cloze" in note.cards()[4].q() note = col.newNote() note["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]" assert col.addNote(note) assert len(note.cards()) == 1 assert ( note.cards()[0] .q() .endswith(r"\(a\) [...] \[ [...] \]") ) def test_typecloze(): col = getEmptyCol() m = col.models.byName("Cloze") col.models.setCurrent(m) m["tmpls"][0]["qfmt"] = "{{cloze:Text}}{{type:cloze:Text}}" col.models.save(m) note = col.newNote() note["Text"] = "hello {{c1::world}}" col.addNote(note) assert "[[type:cloze:Text]]" in note.cards()[0].q() def test_chained_mods(): col = getEmptyCol() col.models.setCurrent(col.models.byName("Cloze")) m = col.models.current() mm = col.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) col.models.remTemplate(m, m["tmpls"][0]) note = col.newNote() q1 = 'phrase' a1 = "sentence" q2 = 'en chaine' a2 = "chained" note["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % ( q1, a1, q2, a2, ) assert col.addNote(note) == 1 assert ( "This [sentence] demonstrates [chained] clozes." in note.cards()[0].q() ) assert ( "This phrase demonstrates en chaine clozes." in note.cards()[0].a() ) def test_modelChange(): col = getEmptyCol() cloze = col.models.byName("Cloze") # enable second template and add a note m = col.models.current() mm = col.models t = mm.newTemplate("Reverse") t["qfmt"] = "{{Back}}" t["afmt"] = "{{Front}}" mm.addTemplate(m, t) mm.save(m) basic = m note = col.newNote() note["Front"] = "note" note["Back"] = "b123" col.addNote(note) # switch fields map = {0: 1, 1: 0} col.models.change(basic, [note.id], basic, map, None) note.load() assert note["Front"] == "b123" assert note["Back"] == "note" # switch cards c0 = note.cards()[0] c1 = note.cards()[1] assert "b123" in c0.q() assert "note" in c1.q() assert c0.ord == 0 assert c1.ord == 1 col.models.change(basic, [note.id], basic, None, map) note.load() c0.load() c1.load() assert "note" in c0.q() assert "b123" in c1.q() assert c0.ord == 1 assert c1.ord == 0 # .cards() returns cards in order assert note.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) col.models.change(basic, [note.id], basic, None, map) note.load() c0.load() # the card was deleted try: c1.load() assert 0 except NotFoundError: pass # but we have two cards, as a new one was generated assert len(note.cards()) == 2 # an unmapped field becomes blank assert note["Front"] == "b123" assert note["Back"] == "note" col.models.change(basic, [note.id], basic, map, None) note.load() assert note["Front"] == "" assert note["Back"] == "note" # another note to try model conversion note = col.newNote() note["Front"] = "f2" note["Back"] = "b2" col.addNote(note) counts = col.models.all_use_counts() assert next(c.use_count for c in counts if c.name == "Basic") == 2 assert next(c.use_count for c in counts if c.name == "Cloze") == 0 map = {0: 0, 1: 1} col.models.change(basic, [note.id], cloze, map, map) note.load() assert note["Text"] == "f2" assert len(note.cards()) == 2 # back the other way, with deletion of second ord col.models.remTemplate(basic, basic["tmpls"][1]) assert col.db.scalar("select count() from cards where nid = ?", note.id) == 2 map = {0: 0} col.models.change(cloze, [note.id], basic, map, map) assert col.db.scalar("select count() from cards where nid = ?", note.id) == 1 def test_req(): def reqSize(model): if model["type"] == MODEL_CLOZE: return assert len(model["tmpls"]) == len(model["req"]) col = getEmptyCol() mm = col.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}}{{/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, 1]