anki/pylib/tests/test_models.py

469 lines
14 KiB
Python
Raw Normal View History

2021-04-13 10:45:05 +02:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# coding: utf-8
import html
import re
import time
from anki.consts import MODEL_CLOZE
from anki.errors import NotFoundError
from anki.utils import is_win, strip_html
from tests.shared import getEmptyCol
2019-12-25 05:18:34 +01:00
def encode_attribute(s):
return "".join(
c if c.isalnum() else "&#x{:X};".format(ord(c)) for c in html.escape(s)
)
def test_modelDelete():
col = getEmptyCol()
note = col.newNote()
note["Front"] = "1"
note["Back"] = "2"
col.addNote(note)
2021-06-27 07:12:22 +02:00
assert col.card_count() == 1
2021-06-27 05:49:58 +02:00
col.models.remove(col.models.current()["id"])
2021-06-27 07:12:22 +02:00
assert col.card_count() == 0
2019-12-25 05:18:34 +01:00
def test_modelCopy():
col = getEmptyCol()
m = col.models.current()
m2 = col.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 col.models.scmhash(m) == col.models.scmhash(m2)
2019-12-25 05:18:34 +01:00
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")
2019-12-25 05:18:34 +01:00
assert "{{NewFront}}" in m["tmpls"][0]["qfmt"]
h = col.models.scmhash(m)
# add a field
2021-06-27 05:49:58 +02:00
field = col.models.new_field("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
2021-06-27 05:49:58 +02:00
field = col.models.new_field("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"]
2019-12-25 05:18:34 +01:00
def test_templates():
col = getEmptyCol()
m = col.models.current()
mm = col.models
2021-06-27 05:49:58 +02:00
t = mm.new_template("Reverse")
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
2021-06-27 05:49:58 +02:00
mm.add_template(m, t)
mm.save(m)
note = col.newNote()
note["Front"] = "1"
note["Back"] = "2"
col.addNote(note)
2021-06-27 07:12:22 +02:00
assert col.card_count() == 2
(c, c2) = note.cards()
# first card should have first ord
assert c.ord == 0
assert c2.ord == 1
# switch templates
2021-06-27 05:49:58 +02:00
col.models.reposition_template(m, c.template(), 1)
col.models.update(m)
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
2021-06-27 05:49:58 +02:00
col.models.remove_template(m, m["tmpls"][0])
col.models.update(m)
2021-06-27 07:12:22 +02:00
assert col.card_count() == 1
# and should have updated the other cards' ordinals
c = note.cards()[0]
assert c.ord == 0
PEP8 for rest of pylib (#1451) * PEP8 dbproxy.py * PEP8 errors.py * PEP8 httpclient.py * PEP8 lang.py * PEP8 latex.py * Add decorator to deprectate key words * Make replacement for deprecated attribute optional * Use new helper `_print_replacement_warning()` * PEP8 media.py * PEP8 rsbackend.py * PEP8 sound.py * PEP8 stdmodels.py * PEP8 storage.py * PEP8 sync.py * PEP8 tags.py * PEP8 template.py * PEP8 types.py * Fix DeprecatedNamesMixinForModule The class methods need to be overridden with instance methods, so every module has its own dicts. * Use `# pylint: disable=invalid-name` instead of id * PEP8 utils.py * Only decorate `__getattr__` with `@no_type_check` * Fix mypy issue with snakecase Importing it from `anki._vendor` raises attribute errors. * Format * Remove inheritance of DeprecatedNamesMixin There's almost no shared code now and overriding classmethods with instance methods raises mypy issues. * Fix traceback frames of deprecation warnings * remove fn/TimedLog (dae) Neither Anki nor add-ons appear to have been using it * fix some issues with stringcase use (dae) - the wheel was depending on the PyPI version instead of our vendored version - _vendor:stringcase should not have been listed in the anki py_library. We already include the sources in py_srcs, and need to refer to them directly. By listing _vendor:stringcase as well, we were making a top-level stringcase library available, which would have only worked for distributing because the wheel definition was also incorrect. - mypy errors are what caused me to mistakenly add the above - they were because the type: ignore at the top of stringcase.py was causing mypy to completely ignore the file, so it was not aware of any attributes it contained.
2021-10-25 06:50:13 +02:00
assert strip_html(c.question()) == "1"
# it shouldn't be possible to orphan notes by removing templates
2021-06-27 05:49:58 +02:00
t = mm.new_template("template name")
t["qfmt"] = "{{Front}}2"
2021-06-27 05:49:58 +02:00
mm.add_template(m, t)
col.models.remove_template(m, m["tmpls"][0])
col.models.update(m)
assert (
col.db.scalar(
"select count() from cards where nid not in (select id from notes)"
)
== 0
)
2019-12-25 05:18:34 +01:00
2014-02-18 18:11:31 +01:00
def test_cloze_ordinals():
col = getEmptyCol()
2021-06-27 05:49:58 +02:00
m = col.models.by_name("Cloze")
mm = col.models
2019-12-25 05:18:34 +01:00
# We replace the default Cloze template
2021-06-27 05:49:58 +02:00
t = mm.new_template("ChainedCloze")
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{text:cloze:Text}}"
t["afmt"] = "{{text:cloze:Text}}"
2021-06-27 05:49:58 +02:00
mm.add_template(m, t)
2014-02-18 18:11:31 +01:00
mm.save(m)
2021-06-27 05:49:58 +02:00
col.models.remove_template(m, m["tmpls"][0])
col.models.update(m)
2019-12-25 05:18:34 +01:00
note = col.newNote()
note["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}"
col.addNote(note)
2021-06-27 07:12:22 +02:00
assert col.card_count() == 2
(c, c2) = note.cards()
2014-02-18 18:11:31 +01:00
# 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():
col = getEmptyCol()
m = col.models.current()
2019-12-25 05:18:34 +01:00
m["tmpls"][0]["qfmt"] = "{{text:Front}}"
col.models.save(m)
note = col.newNote()
note["Front"] = "hello<b>world"
col.addNote(note)
2021-06-27 04:12:23 +02:00
assert "helloworld" in note.cards()[0].question()
2019-12-25 05:18:34 +01:00
def test_cloze():
col = getEmptyCol()
2021-06-27 05:49:58 +02:00
m = col.models.by_name("Cloze")
note = col.new_note(m)
2021-06-27 04:12:23 +02:00
assert note.note_type()["name"] == "Cloze"
# a cloze model with no clozes is not empty
note["Text"] = "nothing"
assert col.addNote(note)
# try with one cloze
2021-06-27 05:49:58 +02:00
note = col.new_note(m)
note["Text"] = "hello {{c1::world}}"
assert col.addNote(note) == 1
assert (
f'hello <span class="cloze" data-cloze="{encode_attribute("world")}" data-ordinal="1">[...]</span>'
in note.cards()[0].question()
)
assert (
'hello <span class="cloze" data-ordinal="1">world</span>'
in note.cards()[0].answer()
)
# and with a comment
2021-06-27 05:49:58 +02:00
note = col.new_note(m)
note["Text"] = "hello {{c1::world::typical}}"
assert col.addNote(note) == 1
assert (
f'<span class="cloze" data-cloze="{encode_attribute("world")}" data-ordinal="1">[typical]</span>'
in note.cards()[0].question()
)
assert (
'<span class="cloze" data-ordinal="1">world</span>' in note.cards()[0].answer()
)
# and with 2 clozes
2021-06-27 05:49:58 +02:00
note = col.new_note(m)
note["Text"] = "hello {{c1::world}} {{c2::bar}}"
assert col.addNote(note) == 2
(c1, c2) = note.cards()
assert (
f'<span class="cloze" data-cloze="{encode_attribute("world")}" data-ordinal="1">[...]</span> <span class="cloze-inactive" data-ordinal="2">bar</span>'
in c1.question()
)
assert (
'<span class="cloze" data-ordinal="1">world</span> <span class="cloze-inactive" data-ordinal="2">bar</span>'
in c1.answer()
)
assert (
f'<span class="cloze-inactive" data-ordinal="1">world</span> <span class="cloze" data-cloze="{encode_attribute("bar")}" data-ordinal="2">[...]</span>'
in c2.question()
)
assert (
'<span class="cloze-inactive" data-ordinal="1">world</span> <span class="cloze" data-ordinal="2">bar</span>'
in c2.answer()
)
# if there are multiple answers for a single cloze, they are given in a
# list
2021-06-27 05:49:58 +02:00
note = col.new_note(m)
note["Text"] = "a {{c1::b}} {{c1::c}}"
assert col.addNote(note) == 1
assert (
'<span class="cloze" data-ordinal="1">b</span> <span class="cloze" data-ordinal="1">c</span>'
in (note.cards()[0].answer())
)
# if we add another cloze, a card should be generated
2021-06-27 07:12:22 +02:00
cnt = col.card_count()
note["Text"] = "{{c2::hello}} {{c1::foo}}"
note.flush()
2021-06-27 07:12:22 +02:00
assert col.card_count() == cnt + 1
# 0 or negative indices are not supported
note["Text"] += "{{c0::zero}} {{c-1:foo}}"
note.flush()
assert len(note.cards()) == 2
2019-12-25 05:18:34 +01:00
2017-09-12 05:53:08 +02:00
def test_cloze_mathjax():
col = getEmptyCol()
2021-06-27 05:49:58 +02:00
m = col.models.by_name("Cloze")
note = col.new_note(m)
q1 = "ok"
q2 = "not ok"
q3 = "2"
q4 = "blah"
q5 = "text with \(x^2\) jax"
note[
2019-12-25 05:18:34 +01:00
"Text"
] = "{{{{c1::{}}}}} \(2^2\) {{{{c2::{}}}}} \(2^{{{{c3::{}}}}}\) \(x^3\) {{{{c4::{}}}}} {{{{c5::{}}}}}".format(
q1,
q2,
q3,
q4,
q5,
)
assert col.addNote(note)
assert len(note.cards()) == 5
assert (
f'class="cloze" data-cloze="{encode_attribute(q1)}"'
in note.cards()[0].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q2)}"'
in note.cards()[1].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q3)}"'
not in note.cards()[2].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q4)}"'
in note.cards()[3].question()
)
assert (
f'class="cloze" data-cloze="{encode_attribute(q5)}"'
in note.cards()[4].question()
)
2017-09-12 05:53:08 +02:00
2021-06-27 05:49:58 +02:00
note = col.new_note(m)
note["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert col.addNote(note)
assert len(note.cards()) == 1
2020-01-03 06:21:36 +01:00
assert (
note.cards()[0]
2021-06-27 04:12:23 +02:00
.question()
.endswith(
r'\(a\) <span class="cloze" data-cloze="b" data-ordinal="1">[...]</span> \[ [...] \]'
)
2020-01-03 06:21:36 +01:00
)
def test_typecloze():
col = getEmptyCol()
2021-06-27 05:49:58 +02:00
m = col.models.by_name("Cloze")
m["tmpls"][0]["qfmt"] = "{{cloze:Text}}{{type:cloze:Text}}"
col.models.save(m)
2021-06-27 05:49:58 +02:00
note = col.new_note(m)
note["Text"] = "hello {{c1::world}}"
col.addNote(note)
2021-06-27 04:12:23 +02:00
assert "[[type:cloze:Text]]" in note.cards()[0].question()
2014-02-18 18:11:31 +01:00
def test_chained_mods():
col = getEmptyCol()
2021-06-27 05:49:58 +02:00
m = col.models.by_name("Cloze")
mm = col.models
2019-12-25 05:18:34 +01:00
# We replace the default Cloze template
2021-06-27 05:49:58 +02:00
t = mm.new_template("ChainedCloze")
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{cloze:text:Text}}"
t["afmt"] = "{{cloze:text:Text}}"
2021-06-27 05:49:58 +02:00
mm.add_template(m, t)
2014-02-18 18:11:31 +01:00
mm.save(m)
2021-06-27 05:49:58 +02:00
col.models.remove_template(m, m["tmpls"][0])
col.models.update(m)
2019-12-25 05:18:34 +01:00
note = col.newNote()
a1 = '<span style="color:red">phrase</span>'
h1 = "<b>sentence</b>"
a2 = '<span style="color:red">en chaine</span>'
h2 = "<i>chained</i>"
note[
"Text"
] = "This {{{{c1::{}::{}}}}} demonstrates {{{{c1::{}::{}}}}} clozes.".format(
2019-12-25 05:18:34 +01:00
a1,
h1,
2019-12-25 05:18:34 +01:00
a2,
h2,
2019-12-25 05:18:34 +01:00
)
assert col.addNote(note) == 1
2019-12-25 05:18:34 +01:00
assert (
'This <span class="cloze" data-cloze="phrase" data-ordinal="1">[sentence]</span>'
f' demonstrates <span class="cloze" data-cloze="{encode_attribute("en chaine")}" data-ordinal="1">[chained]</span> clozes.'
2021-06-27 04:12:23 +02:00
in note.cards()[0].question()
2019-12-25 05:18:34 +01:00
)
assert (
'This <span class="cloze" data-ordinal="1">phrase</span> demonstrates <span class="cloze" data-ordinal="1">en chaine</span> clozes.'
2021-06-27 04:12:23 +02:00
in note.cards()[0].answer()
2019-12-25 05:18:34 +01:00
)
2014-02-18 18:11:31 +01:00
def test_modelChange():
col = getEmptyCol()
2021-06-27 05:49:58 +02:00
cloze = col.models.by_name("Cloze")
# enable second template and add a note
m = col.models.current()
mm = col.models
2021-06-27 05:49:58 +02:00
t = mm.new_template("Reverse")
2019-12-25 05:18:34 +01:00
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
2021-06-27 05:49:58 +02:00
mm.add_template(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}
noop = {0: 0, 1: 1}
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]
2021-06-27 04:12:23 +02:00
assert "b123" in c0.question()
assert "note" in c1.question()
assert c0.ord == 0
assert c1.ord == 1
col.models.change(basic, [note.id], basic, noop, map)
note.load()
2019-12-25 05:18:34 +01:00
c0.load()
c1.load()
2021-06-27 04:12:23 +02:00
assert "note" in c0.question()
assert "b123" in c1.question()
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}
time.sleep(0.25)
col.models.change(basic, [note.id], basic, noop, map)
note.load()
c0.load()
# the card was deleted
try:
c1.load()
assert 0
2020-05-23 08:19:48 +02:00
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
2021-06-27 05:49:58 +02:00
col.models.remove_template(basic, basic["tmpls"][1])
col.models.update(basic)
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
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
col = getEmptyCol()
mm = col.models
2021-06-27 05:49:58 +02:00
basic = mm.by_name("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]
2021-06-27 05:49:58 +02:00
opt = mm.by_name("Basic (optional reversed card)")
2019-12-15 19:40:38 +01:00
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}}{{Tags}}{{/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
2021-06-27 05:49:58 +02:00
opt = mm.by_name("Basic (type in the answer)")
2019-12-25 04:01:19 +01:00
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, 1]