2012-12-21 08:51:59 +01:00
|
|
|
# coding: utf-8
|
|
|
|
|
2019-12-25 22:36:26 +01:00
|
|
|
import os
|
|
|
|
import tempfile
|
2019-12-24 10:14:48 +01:00
|
|
|
|
2019-12-25 22:36:26 +01:00
|
|
|
from anki import Collection as aopen
|
2020-04-06 12:09:44 +02:00
|
|
|
from anki.dbproxy import emulate_named_args
|
2021-01-31 06:55:08 +01:00
|
|
|
from anki.lang import TR, without_unicode_isolation
|
2020-05-15 08:05:58 +02:00
|
|
|
from anki.stdmodels import addBasicModel, get_stock_notetypes
|
2019-12-24 10:14:48 +01:00
|
|
|
from anki.utils import isWin
|
2014-06-03 10:38:47 +02:00
|
|
|
from tests.shared import assertException, getEmptyCol
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-25 05:18:34 +01:00
|
|
|
|
2017-10-05 06:14:56 +02:00
|
|
|
def test_create_open():
|
2014-04-21 23:04:46 +02:00
|
|
|
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
|
2012-12-21 08:51:59 +01:00
|
|
|
try:
|
2014-04-21 23:04:46 +02:00
|
|
|
os.close(fd)
|
2012-12-21 08:51:59 +01:00
|
|
|
os.unlink(path)
|
|
|
|
except OSError:
|
|
|
|
pass
|
2020-07-17 05:21:01 +02:00
|
|
|
col = aopen(path)
|
2012-12-21 08:51:59 +01:00
|
|
|
# for open()
|
2020-07-17 05:21:01 +02:00
|
|
|
newPath = col.path
|
|
|
|
newMod = col.mod
|
|
|
|
col.close()
|
|
|
|
del col
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-10-05 06:14:56 +02:00
|
|
|
# reopen
|
2020-07-17 05:21:01 +02:00
|
|
|
col = aopen(newPath)
|
|
|
|
assert col.mod == newMod
|
|
|
|
col.close()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# non-writeable dir
|
2019-12-24 10:14:48 +01:00
|
|
|
if isWin:
|
|
|
|
dir = "c:\root.anki2"
|
|
|
|
else:
|
|
|
|
dir = "/attachroot.anki2"
|
2019-12-25 05:18:34 +01:00
|
|
|
assertException(Exception, lambda: aopen(dir))
|
2012-12-21 08:51:59 +01:00
|
|
|
# reuse tmp file from before, test non-writeable file
|
|
|
|
os.chmod(newPath, 0)
|
2019-12-25 05:18:34 +01:00
|
|
|
assertException(Exception, lambda: aopen(newPath))
|
2016-05-12 06:45:35 +02:00
|
|
|
os.chmod(newPath, 0o666)
|
2012-12-21 08:51:59 +01:00
|
|
|
os.unlink(newPath)
|
|
|
|
|
2019-12-25 05:18:34 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def test_noteAddDelete():
|
2020-07-17 05:21:01 +02:00
|
|
|
col = getEmptyCol()
|
2012-12-21 08:51:59 +01:00
|
|
|
# add a note
|
2020-07-17 05:21:01 +02:00
|
|
|
note = col.newNote()
|
2020-07-17 05:18:09 +02:00
|
|
|
note["Front"] = "one"
|
|
|
|
note["Back"] = "two"
|
2020-07-17 05:21:01 +02:00
|
|
|
n = col.addNote(note)
|
2012-12-21 08:51:59 +01:00
|
|
|
assert n == 1
|
|
|
|
# test multiple cards - add another template
|
2020-07-17 05:21:01 +02:00
|
|
|
m = col.models.current()
|
|
|
|
mm = col.models
|
2012-12-21 08:51:59 +01:00
|
|
|
t = mm.newTemplate("Reverse")
|
2019-12-25 05:18:34 +01:00
|
|
|
t["qfmt"] = "{{Back}}"
|
|
|
|
t["afmt"] = "{{Front}}"
|
2012-12-21 08:51:59 +01:00
|
|
|
mm.addTemplate(m, t)
|
|
|
|
mm.save(m)
|
2020-07-17 05:21:01 +02:00
|
|
|
assert col.cardCount() == 2
|
2012-12-21 08:51:59 +01:00
|
|
|
# creating new notes should use both cards
|
2020-07-17 05:21:01 +02:00
|
|
|
note = col.newNote()
|
2020-07-17 05:18:09 +02:00
|
|
|
note["Front"] = "three"
|
|
|
|
note["Back"] = "four"
|
2020-07-17 05:21:01 +02:00
|
|
|
n = col.addNote(note)
|
2012-12-21 08:51:59 +01:00
|
|
|
assert n == 2
|
2020-07-17 05:21:01 +02:00
|
|
|
assert col.cardCount() == 4
|
2012-12-21 08:51:59 +01:00
|
|
|
# check q/a generation
|
2020-07-17 05:18:09 +02:00
|
|
|
c0 = note.cards()[0]
|
2012-12-21 08:51:59 +01:00
|
|
|
assert "three" in c0.q()
|
|
|
|
# it should not be a duplicate
|
Simplify note adding and the deck/notetype choosers
The existing code was really difficult to reason about:
- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.
It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.
To address these points:
- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
2021-03-08 14:23:24 +01:00
|
|
|
assert not note.duplicate_or_empty()
|
2012-12-21 08:51:59 +01:00
|
|
|
# now let's make a duplicate
|
2020-07-17 05:21:01 +02:00
|
|
|
note2 = col.newNote()
|
2020-07-17 05:18:35 +02:00
|
|
|
note2["Front"] = "one"
|
|
|
|
note2["Back"] = ""
|
Simplify note adding and the deck/notetype choosers
The existing code was really difficult to reason about:
- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.
It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.
To address these points:
- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
2021-03-08 14:23:24 +01:00
|
|
|
assert note2.duplicate_or_empty()
|
2012-12-21 08:51:59 +01:00
|
|
|
# empty first field should not be permitted either
|
2020-07-17 05:18:35 +02:00
|
|
|
note2["Front"] = " "
|
Simplify note adding and the deck/notetype choosers
The existing code was really difficult to reason about:
- The default notetype depended on the selected deck, and vice versa,
and this logic was buried in the deck and notetype choosing screens,
and models.py.
- Changes to the notetype were not passed back directly, but were fired
via a hook, which changed any screen in the app that had a notetype
selector.
It also wasn't great for performance, as the most recent deck and tags
were embedded in the notetype, which can be expensive to save and sync
for large notetypes.
To address these points:
- The current deck for a notetype, and notetype for a deck, are now
stored in separate config variables, instead of directly in the deck
or notetype. These are cheap to read and write, and we'll be able to
sync them individually in the future once config syncing is updated in
the future. I seem to recall some users not wanting the tag saving
behaviour, so I've dropped that for now, but if people end up missing
it, it would be simple to add as an extra auxiliary config variable.
- The logic for getting the starting deck and notetype has been moved
into the backend. It should be the same as the older Python code, with
one exception: when "change deck depending on notetype" is enabled in
the preferences, it will start with the current notetype ("curModel"),
instead of first trying to get a deck-specific notetype.
- ModelChooser has been duplicated into notetypechooser.py, and it
has been updated to solely be concerned with keeping track of a selected
notetype - it no longer alters global state.
2021-03-08 14:23:24 +01:00
|
|
|
assert note2.duplicate_or_empty()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-25 05:18:34 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def test_fieldChecksum():
|
2020-07-17 05:21:01 +02:00
|
|
|
col = getEmptyCol()
|
|
|
|
note = col.newNote()
|
2020-07-17 05:18:09 +02:00
|
|
|
note["Front"] = "new"
|
|
|
|
note["Back"] = "new2"
|
2020-07-17 05:21:01 +02:00
|
|
|
col.addNote(note)
|
|
|
|
assert col.db.scalar("select csum from notes") == int("c2a6b03f", 16)
|
2012-12-21 08:51:59 +01:00
|
|
|
# changing the val should change the checksum
|
2020-07-17 05:18:09 +02:00
|
|
|
note["Front"] = "newx"
|
|
|
|
note.flush()
|
2020-07-17 05:21:01 +02:00
|
|
|
assert col.db.scalar("select csum from notes") == int("302811ae", 16)
|
2019-12-25 05:18:34 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def test_addDelTags():
|
2020-07-17 05:21:01 +02:00
|
|
|
col = getEmptyCol()
|
|
|
|
note = col.newNote()
|
2020-07-17 05:18:09 +02:00
|
|
|
note["Front"] = "1"
|
2020-07-17 05:21:01 +02:00
|
|
|
col.addNote(note)
|
|
|
|
note2 = col.newNote()
|
2020-07-17 05:18:35 +02:00
|
|
|
note2["Front"] = "2"
|
2020-07-17 05:21:01 +02:00
|
|
|
col.addNote(note2)
|
2012-12-21 08:51:59 +01:00
|
|
|
# adding for a given id
|
2021-03-05 11:47:51 +01:00
|
|
|
col.tags.bulk_add([note.id], "foo")
|
2020-07-17 05:18:09 +02:00
|
|
|
note.load()
|
2020-07-17 05:18:35 +02:00
|
|
|
note2.load()
|
2020-07-17 05:18:09 +02:00
|
|
|
assert "foo" in note.tags
|
2020-07-17 05:18:35 +02:00
|
|
|
assert "foo" not in note2.tags
|
2012-12-21 08:51:59 +01:00
|
|
|
# should be canonified
|
2021-03-05 11:47:51 +01:00
|
|
|
col.tags.bulk_add([note.id], "foo aaa")
|
2020-07-17 05:18:09 +02:00
|
|
|
note.load()
|
|
|
|
assert note.tags[0] == "aaa"
|
|
|
|
assert len(note.tags) == 2
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-25 05:18:34 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def test_timestamps():
|
2020-07-17 05:21:01 +02:00
|
|
|
col = getEmptyCol()
|
|
|
|
assert len(col.models.all_names_and_ids()) == len(get_stock_notetypes(col))
|
2012-12-21 08:51:59 +01:00
|
|
|
for i in range(100):
|
2020-07-17 05:21:01 +02:00
|
|
|
addBasicModel(col)
|
|
|
|
assert len(col.models.all_names_and_ids()) == 100 + len(get_stock_notetypes(col))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-25 05:18:34 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def test_furigana():
|
2020-07-17 05:21:01 +02:00
|
|
|
col = getEmptyCol()
|
|
|
|
mm = col.models
|
2012-12-21 08:51:59 +01:00
|
|
|
m = mm.current()
|
|
|
|
# filter should work
|
2019-12-25 05:18:34 +01:00
|
|
|
m["tmpls"][0]["qfmt"] = "{{kana:Front}}"
|
2012-12-21 08:51:59 +01:00
|
|
|
mm.save(m)
|
2020-07-17 05:21:01 +02:00
|
|
|
n = col.newNote()
|
2019-12-25 05:18:34 +01:00
|
|
|
n["Front"] = "foo[abc]"
|
2020-07-17 05:21:01 +02:00
|
|
|
col.addNote(n)
|
2012-12-21 08:51:59 +01:00
|
|
|
c = n.cards()[0]
|
|
|
|
assert c.q().endswith("abc")
|
|
|
|
# and should avoid sound
|
2019-12-25 05:18:34 +01:00
|
|
|
n["Front"] = "foo[sound:abc.mp3]"
|
2012-12-21 08:51:59 +01:00
|
|
|
n.flush()
|
2020-01-24 02:06:11 +01:00
|
|
|
assert "anki:play" in c.q(reload=True)
|
2012-12-21 08:51:59 +01:00
|
|
|
# it shouldn't throw an error while people are editing
|
2019-12-25 05:18:34 +01:00
|
|
|
m["tmpls"][0]["qfmt"] = "{{kana:}}"
|
2012-12-21 08:51:59 +01:00
|
|
|
mm.save(m)
|
|
|
|
c.q(reload=True)
|
2020-02-16 12:07:40 +01:00
|
|
|
|
|
|
|
|
|
|
|
def test_translate():
|
2020-07-17 05:21:12 +02:00
|
|
|
col = getEmptyCol()
|
2020-02-20 03:48:46 +01:00
|
|
|
no_uni = without_unicode_isolation
|
2020-02-16 12:07:40 +01:00
|
|
|
|
2020-02-23 03:21:12 +01:00
|
|
|
assert (
|
2021-03-26 03:37:18 +01:00
|
|
|
col.tr.card_template_rendering_front_side_problem()
|
2020-02-23 03:21:12 +01:00
|
|
|
== "Front template has a problem:"
|
|
|
|
)
|
2021-03-26 05:21:04 +01:00
|
|
|
assert no_uni(col.tr.statistics_reviews(reviews=1)) == "1 review"
|
|
|
|
assert no_uni(col.tr.statistics_reviews(reviews=2)) == "2 reviews"
|
2020-04-06 12:09:44 +02:00
|
|
|
|
|
|
|
|
2020-04-06 12:24:05 +02:00
|
|
|
def test_db_named_args(capsys):
|
2020-04-06 12:09:44 +02:00
|
|
|
sql = "select a, 2+:test5 from b where arg =:foo and x = :test5"
|
|
|
|
args = []
|
|
|
|
kwargs = dict(test5=5, foo="blah")
|
|
|
|
|
|
|
|
s, a = emulate_named_args(sql, args, kwargs)
|
|
|
|
assert s == "select a, 2+?1 from b where arg =?2 and x = ?1"
|
|
|
|
assert a == [5, "blah"]
|
2020-04-06 12:24:05 +02:00
|
|
|
|
|
|
|
# swallow the warning
|
|
|
|
_ = capsys.readouterr()
|