anki/tests/test_models.py

426 lines
13 KiB
Python
Raw Normal View History

# coding: utf-8
import time
2016-06-30 14:23:31 +02:00
import anki.template
from anki.consts import MODEL_CLOZE
from anki.utils import isWin, joinFields, stripHTML
from tests.shared import getEmptyCol
2019-12-25 05:18:34 +01:00
def test_modelDelete():
2014-06-03 10:38:47 +02:00
deck = getEmptyCol()
f = deck.newNote()
2019-12-25 05:18:34 +01:00
f["Front"] = "1"
f["Back"] = "2"
deck.addNote(f)
assert deck.cardCount() == 1
deck.models.rem(deck.models.current())
assert deck.cardCount() == 0
2019-12-25 05:18:34 +01:00
def test_modelCopy():
2014-06-03 10:38:47 +02:00
deck = getEmptyCol()
m = deck.models.current()
m2 = deck.models.copy(m)
2019-12-25 05:18:34 +01:00
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)
2019-12-25 05:18:34 +01:00
def test_fields():
2014-06-03 10:38:47 +02:00
d = getEmptyCol()
f = d.newNote()
2019-12-25 05:18:34 +01:00
f["Front"] = "1"
f["Back"] = "2"
d.addNote(f)
m = d.models.current()
# make sure renaming a field updates the templates
2019-12-25 05:18:34 +01:00
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")
2019-12-25 05:18:34 +01:00
assert d.getNote(d.models.nids(m)[0])["bar"] == ""
# delete back
2019-12-25 05:18:34 +01:00
d.models.remField(m, m["flds"][1])
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
# move 0 -> 1
2019-12-25 05:18:34 +01:00
d.models.moveField(m, m["flds"][0], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"]
# move 1 -> 0
2019-12-25 05:18:34 +01:00
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])
2019-12-25 05:18:34 +01:00
f["baz"] = "2"
f.flush()
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
# move 2 -> 1
2019-12-25 05:18:34 +01:00
d.models.moveField(m, m["flds"][2], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
# move 0 -> 2
2019-12-25 05:18:34 +01:00
d.models.moveField(m, m["flds"][0], 2)
assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"]
# move 0 -> 1
2019-12-25 05:18:34 +01:00
d.models.moveField(m, m["flds"][0], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
2019-12-25 05:18:34 +01:00
def test_templates():
2014-06-03 10:38:47 +02:00
d = getEmptyCol()
2019-12-25 05:18:34 +01:00
m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse")
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = d.newNote()
2019-12-25 05:18:34 +01:00
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)
2019-12-25 05:18:34 +01:00
c.load()
c2.load()
assert c.ord == 1
assert c2.ord == 0
# removing a template should delete its cards
2019-12-25 05:18:34 +01:00
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")
mm.addTemplate(m, t)
2019-12-25 05:18:34 +01:00
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"))
2019-12-25 05:18:34 +01:00
m = d.models.current()
mm = d.models
# We replace the default Cloze template
2014-02-18 18:11:31 +01:00
t = mm.newTemplate("ChainedCloze")
2019-12-25 05:18:34 +01: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)
2019-12-25 05:18:34 +01:00
d.models.remTemplate(m, m["tmpls"][0])
2014-02-18 18:11:31 +01:00
f = d.newNote()
2019-12-25 05:18:34 +01: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
2019-12-25 05:18:34 +01:00
2014-02-18 18:11:31 +01:00
def test_text():
2014-06-03 10:38:47 +02:00
d = getEmptyCol()
m = d.models.current()
2019-12-25 05:18:34 +01:00
m["tmpls"][0]["qfmt"] = "{{text:Front}}"
d.models.save(m)
f = d.newNote()
2019-12-25 05:18:34 +01:00
f["Front"] = "hello<b>world"
d.addNote(f)
assert "helloworld" in f.cards()[0].q()
2019-12-25 05:18:34 +01:00
def test_cloze():
2014-06-03 10:38:47 +02:00
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote()
2019-12-25 05:18:34 +01:00
assert f.model()["name"] == "Cloze"
# a cloze model with no clozes is not empty
2019-12-25 05:18:34 +01:00
f["Text"] = "nothing"
assert d.addNote(f)
# try with one cloze
f = d.newNote()
2019-12-25 05:18:34 +01:00
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()
2019-12-25 05:18:34 +01:00
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()
2019-12-25 05:18:34 +01:00
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()
2019-12-25 05:18:34 +01:00
f["Text"] = "a {{c1::b}} {{c1::c}}"
assert d.addNote(f) == 1
2019-12-25 05:18:34 +01:00
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()
2019-12-25 05:18:34 +01:00
f["Text"] = "{{c2::hello}} {{c1::foo}}"
f.flush()
assert d.cardCount() == cnt + 1
# 0 or negative indices are not supported
2019-12-25 05:18:34 +01:00
f["Text"] += "{{c0::zero}} {{c-1:foo}}"
f.flush()
assert len(f.cards()) == 2
2019-12-25 05:18:34 +01:00
2017-09-12 05:53:08 +02:00
def test_cloze_mathjax():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote()
2019-12-25 05:18:34 +01:00
f[
"Text"
] = r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}"
2017-09-12 05:53:08 +02:00
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()
2019-12-25 05:18:34 +01:00
f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert d.addNote(f)
assert len(f.cards()) == 1
2019-12-25 05:18:34 +01:00
assert f.cards()[0].q().endswith("\(a\) <span class=cloze>[...]</span> \[ [...] \]")
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"))
2019-12-25 05:18:34 +01:00
m = d.models.current()
mm = d.models
# We replace the default Cloze template
2014-02-18 18:11:31 +01:00
t = mm.newTemplate("ChainedCloze")
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{cloze:text:Text}}"
t["afmt"] = "{{cloze:text:Text}}"
2014-02-18 18:11:31 +01:00
mm.addTemplate(m, t)
mm.save(m)
2019-12-25 05:18:34 +01:00
d.models.remTemplate(m, m["tmpls"][0])
2014-02-18 18:11:31 +01:00
f = d.newNote()
2019-12-25 05:18:34 +01:00
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,
)
2014-02-18 18:11:31 +01:00
assert d.addNote(f) == 1
2019-12-25 05:18:34 +01:00
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()
)
2014-02-18 18:11:31 +01:00
def test_modelChange():
2014-06-03 10:38:47 +02:00
deck = getEmptyCol()
basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze")
# enable second template and add a note
2019-12-25 05:18:34 +01:00
m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse")
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = deck.newNote()
2019-12-25 05:18:34 +01:00
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()
2019-12-25 05:18:34 +01:00
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)
2019-12-25 05:18:34 +01:00
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
2019-12-25 05:18:34 +01:00
assert f["Front"] == "b123"
assert f["Back"] == "f"
deck.models.change(basic, [f.id], basic, map, None)
f.load()
2019-12-25 05:18:34 +01:00
assert f["Front"] == ""
assert f["Back"] == "f"
# another note to try model conversion
f = deck.newNote()
2019-12-25 05:18:34 +01:00
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()
2019-12-25 05:18:34 +01:00
assert f["Text"] == "f2"
assert len(f.cards()) == 2
# back the other way, with deletion of second ord
2019-12-25 05:18:34 +01:00
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
2019-12-25 05:18:34 +01:00
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) == ""
2019-12-25 05:18:34 +01:00
def test_availOrds():
2014-06-03 10:38:47 +02:00
d = getEmptyCol()
2019-12-25 05:18:34 +01:00
m = d.models.current()
mm = d.models
t = m["tmpls"][0]
f = d.newNote()
2019-12-25 05:18:34 +01:00
f["Front"] = "1"
# simple templates
assert mm.availOrds(m, joinFields(f.fields)) == [0]
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{Back}}"
mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields))
# AND
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields))
2019-12-25 05:18:34 +01:00
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
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{Front}}\n{{Back}}"
mm.save(m, templates=True)
assert mm.availOrds(m, joinFields(f.fields)) == [0]
2019-12-25 05:18:34 +01:00
t["Front"] = ""
t["Back"] = "1"
assert mm.availOrds(m, joinFields(f.fields)) == [0]
2019-12-15 19:40:38 +01:00
2019-12-25 05:18:34 +01:00
2019-12-15 19:40:38 +01:00
def test_req():
def reqSize(model):
2019-12-25 05:18:34 +01:00
if model["type"] == MODEL_CLOZE:
2019-12-15 19:40:38 +01:00
return
2019-12-25 05:18:34 +01:00
assert len(model["tmpls"]) == len(model["req"])
2019-12-15 19:40:38 +01:00
d = getEmptyCol()
mm = d.models
basic = mm.byName("Basic")
2019-12-25 05:18:34 +01:00
assert "req" in basic
2019-12-15 19:40:38 +01:00
reqSize(basic)
2019-12-25 05:18:34 +01:00
r = basic["req"][0]
assert r[0] == 0
assert r[1] in ("any", "all")
assert r[2] == [0]
2019-12-15 19:40:38 +01:00
opt = mm.byName("Basic (optional reversed card)")
reqSize(opt)
2019-12-25 05:18:34 +01:00
r = opt["req"][0]
assert r[1] in ("any", "all")
assert r[2] == [0]
2019-12-25 05:18:34 +01:00
assert opt["req"][1] == [1, "all", [1, 2]]
# testing any
opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}"
2019-12-15 19:40:38 +01:00
mm.save(opt, templates=True)
2019-12-25 05:18:34 +01:00
assert opt["req"][1] == [1, "any", [1, 2]]
# testing None
opt["tmpls"][1]["qfmt"] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
2019-12-15 19:40:38 +01:00
mm.save(opt, templates=True)
2019-12-25 05:18:34 +01:00
assert opt["req"][1] == [1, "none", []]
2019-12-24 01:23:21 +01:00
2019-12-25 04:01:19 +01:00
opt = mm.byName("Basic (type in the answer)")
reqSize(opt)
2019-12-25 05:18:34 +01:00
r = opt["req"][0]
2019-12-25 04:01:19 +01:00
assert r[1] in ("any", "all")
assert r[2] == [0]
2019-12-25 05:18:34 +01:00
2019-12-24 10:49:31 +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)