def test_anki2(): global srcNotes, srcCards # get the deck to import tmp = getUpgradeDeckPath() u = Upgrader() u.check(tmp) src = u.upgrade() srcpath = src.path srcNotes = src.noteCount() srcCards = src.cardCount() srcRev = src.db.scalar("select count() from revlog") # add a media file for testing open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo") src.close() # create a new empty deck dst = getEmptyCol() # import src into dst imp = Anki2Importer(dst, srcpath) imp.run() def check(): assert dst.noteCount() == srcNotes assert dst.cardCount() == srcCards assert srcRev == dst.db.scalar("select count() from revlog") mids = [int(x) for x in dst.models.models.keys()] assert not dst.db.scalar("select count() from notes where mid not in " + ids2str(mids)) assert not dst.db.scalar("select count() from cards where nid not in (select id from notes)") assert not dst.db.scalar("select count() from revlog where cid not in (select id from cards)") assert dst.fixIntegrity()[0].startswith("Database rebuilt") check() # importing should be idempotent imp.run() check() assert len(os.listdir(dst.media.dir())) == 1
def test_overdue_lapse(): # disabled in commit 3069729776990980f34c25be66410e947e9d51a2 return d = getEmptyCol() # add a note f = d.newNote() f['Front'] = u"one" d.addNote(f) # simulate a review that was lapsed and is now due for its normal review c = f.cards()[0] c.type = 2 c.queue = 1 c.due = -1 c.odue = -1 c.factor = 2500 c.left = 2002 c.ivl = 0 c.flush() d.sched._clearOverdue = False # checkpoint d.save() d.sched.reset() assert d.sched.counts() == (0, 2, 0) c = d.sched.getCard() d.sched.answerCard(c, 3) # it should be due tomorrow assert c.due == d.sched.today + 1 # revert to before d.rollback() d.sched._clearOverdue = True # with the default settings, the overdue card should be removed from the # learning queue d.sched.reset() assert d.sched.counts() == (0, 0, 1)
def test_anki1_diffmodels(): # create a new empty deck dst = getEmptyCol() # import the 1 card version of the model tmp = getUpgradeDeckPath("diffmodels1.anki") imp = Anki1Importer(dst, tmp) imp.run() before = dst.noteCount() # repeating the process should do nothing imp = Anki1Importer(dst, tmp) imp.run() assert before == dst.noteCount() # then the 2 card version tmp = getUpgradeDeckPath("diffmodels2.anki") imp = Anki1Importer(dst, tmp) imp.run() after = dst.noteCount() # as the model schemas differ, should have been imported as new model assert after == before + 1 # repeating the process should do nothing beforeModels = len(dst.models.all()) imp = Anki1Importer(dst, tmp) imp.run() after = dst.noteCount() assert after == before + 1 assert beforeModels == len(dst.models.all())
def _test_includes_bad_command(bad): d = getEmptyCol() f = d.newNote() f['Front'] = '[latex]%s[/latex]' % bad d.addNote(f) q = f.cards()[0].q() return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q)
def test_learn_collapsed(): d = getEmptyCol() # add 2 notes f = d.newNote() f['Front'] = u"1" f = d.addNote(f) f = d.newNote() f['Front'] = u"2" f = d.addNote(f) # set as a learn card and rebuild queues d.db.execute("update cards set queue=0, type=0") d.reset() # should get '1' first c = d.sched.getCard() assert c.q().endswith("1") # pass it so it's due in 10 minutes d.sched.answerCard(c, 2) # get the other card c = d.sched.getCard() assert c.q().endswith("2") # fail it so it's due in 1 minute d.sched.answerCard(c, 1) # we shouldn't get the same card again c = d.sched.getCard() assert not c.q().endswith("2")
def test_remove(): deck = getEmptyCol() # create a new deck, and add a note/card to it g1 = deck.decks.id("g1") f = deck.newNote() f["Front"] = u"1" f.model()["did"] = g1 deck.addNote(f) c = f.cards()[0] assert c.did == g1 # by default deleting the deck leaves the cards with an invalid did assert deck.cardCount() == 1 deck.decks.rem(g1) assert deck.cardCount() == 1 c.load() assert c.did == g1 # but if we try to get it, we get the default assert deck.decks.name(c.did) == "[no deck]" # let's create another deck and explicitly set the card to it g2 = deck.decks.id("g2") c.did = g2 c.flush() # this time we'll delete the card/note too deck.decks.rem(g2, cardsToo=True) assert deck.cardCount() == 0 assert deck.noteCount() == 0
def test_anki2_updates(): # create a new empty deck dst = getEmptyCol() tmp = getUpgradeDeckPath("update1.apkg") imp = AnkiPackageImporter(dst, tmp) imp.run() assert imp.dupes == 0 assert imp.added == 1 assert imp.updated == 0 # importing again should be idempotent imp = AnkiPackageImporter(dst, tmp) imp.run() assert imp.dupes == 1 assert imp.added == 0 assert imp.updated == 0 # importing a newer note should update assert dst.noteCount() == 1 assert dst.db.scalar("select flds from notes").startswith("hello") tmp = getUpgradeDeckPath("update2.apkg") imp = AnkiPackageImporter(dst, tmp) imp.run() assert imp.dupes == 1 assert imp.added == 0 assert imp.updated == 1 assert dst.noteCount() == 1 assert dst.db.scalar("select flds from notes").startswith("goodbye")
def test_newLimits(): d = getEmptyCol() # add some notes g2 = d.decks.id("Default::foo") for i in range(30): f = d.newNote() f['Front'] = str(i) if i > 4: f.model()['did'] = g2 d.addNote(f) # give the child deck a different configuration c2 = d.decks.confId("new conf") d.decks.setConf(d.decks.get(g2), c2) d.reset() # both confs have defaulted to a limit of 20 assert d.sched.newCount == 20 # first card we get comes from parent c = d.sched.getCard() assert c.did == 1 # limit the parent to 10 cards, meaning we get 10 in total conf1 = d.decks.confForDid(1) conf1['new']['perDay'] = 10 d.reset() assert d.sched.newCount == 10 # if we limit child to 4, we should get 9 conf2 = d.decks.confForDid(g2) conf2['new']['perDay'] = 4 d.reset() assert d.sched.newCount == 9
def test_findDupes(): deck = getEmptyCol() f = deck.newNote() f['Front'] = 'foo' f['Back'] = 'bar' deck.addNote(f) f2 = deck.newNote() f2['Front'] = 'baz' f2['Back'] = 'bar' deck.addNote(f2) f3 = deck.newNote() f3['Front'] = 'quux' f3['Back'] = 'bar' deck.addNote(f3) f4 = deck.newNote() f4['Front'] = 'quuux' f4['Back'] = 'nope' deck.addNote(f4) r = deck.findDupes("Back") assert r[0][0] == "bar" assert len(r[0][1]) == 3 # valid search r = deck.findDupes("Back", "bar") assert r[0][0] == "bar" assert len(r[0][1]) == 3 # excludes everything r = deck.findDupes("Back", "invalid") assert not r # front isn't dupe assert deck.findDupes("Front") == []
def test_findReplace(): deck = getEmptyCol() f = deck.newNote() f['Front'] = 'foo' f['Back'] = 'bar' deck.addNote(f) f2 = deck.newNote() f2['Front'] = 'baz' f2['Back'] = 'foo' deck.addNote(f2) nids = [f.id, f2.id] # should do nothing assert deck.findReplace(nids, "abc", "123") == 0 # global replace assert deck.findReplace(nids, "foo", "qux") == 2 f.load(); assert f['Front'] == "qux" f2.load(); assert f2['Back'] == "qux" # single field replace assert deck.findReplace(nids, "qux", "foo", field="Front") == 1 f.load(); assert f['Front'] == "foo" f2.load(); assert f2['Back'] == "qux" # regex replace assert deck.findReplace(nids, "B.r", "reg") == 0 f.load(); assert f['Back'] != "reg" assert deck.findReplace(nids, "B.r", "reg", regex=True) == 1 f.load(); assert f['Back'] == "reg"
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(m) mm.addTemplate(m, t) assert not d.models.remTemplate(m, m['tmpls'][0])
def test_anki2_diffmodels(): # create a new empty deck dst = getEmptyCol() # import the 1 card version of the model tmp = getUpgradeDeckPath("diffmodels2-1.apkg") imp = AnkiPackageImporter(dst, tmp) imp.dupeOnSchemaChange = True imp.run() before = dst.noteCount() # repeating the process should do nothing imp = AnkiPackageImporter(dst, tmp) imp.dupeOnSchemaChange = True imp.run() assert before == dst.noteCount() # then the 2 card version tmp = getUpgradeDeckPath("diffmodels2-2.apkg") imp = AnkiPackageImporter(dst, tmp) imp.dupeOnSchemaChange = True imp.run() after = dst.noteCount() # as the model schemas differ, should have been imported as new model assert after == before + 1 # and the new model should have both cards assert dst.cardCount() == 3 # repeating the process should do nothing imp = AnkiPackageImporter(dst, tmp) imp.dupeOnSchemaChange = True imp.run() after = dst.noteCount() assert after == before + 1 assert dst.cardCount() == 3
def test_findDupes(): deck = getEmptyCol() f = deck.newNote() f["Front"] = "foo" f["Back"] = "bar" deck.addNote(f) f2 = deck.newNote() f2["Front"] = "baz" f2["Back"] = "bar" deck.addNote(f2) f3 = deck.newNote() f3["Front"] = "quux" f3["Back"] = "bar" deck.addNote(f3) f4 = deck.newNote() f4["Front"] = "quuux" f4["Back"] = "nope" deck.addNote(f4) r = deck.findDupes("Back") assert r[0][0] == "bar" assert len(r[0][1]) == 3 # valid search r = deck.findDupes("Back", "bar") assert r[0][0] == "bar" assert len(r[0][1]) == 3 # excludes everything r = deck.findDupes("Back", "invalid") assert not r # front isn't dupe assert deck.findDupes("Front") == []
def test_gendeck(): d = getEmptyCol() cloze = d.models.byName("Cloze") d.models.setCurrent(cloze) f = d.newNote() f["Text"] = u"{{c1::one}}" d.addNote(f) assert d.cardCount() == 1 assert f.cards()[0].did == 1 # set the model to a new default deck newId = d.decks.id("new") cloze["did"] = newId d.models.save(cloze) # a newly generated card should share the first card's deck f["Text"] += u"{{c2::two}}" f.flush() assert f.cards()[1].did == 1 # and same with multiple cards f["Text"] += u"{{c3::three}}" f.flush() assert f.cards()[2].did == 1 # if one of the cards is in a different deck, it should revert to the # model default c = f.cards()[1] c.did = newId c.flush() f["Text"] += u"{{c4::four}}" f.flush() assert f.cards()[3].did == newId
def test_csv(): deck = getEmptyCol() file = str(os.path.join(testDir, "support/text-2fields.txt")) i = TextImporter(deck, file) i.initMapping() i.run() # four problems - too many & too few fields, a missing front, and a # duplicate entry assert len(i.log) == 5 assert i.total == 5 # if we run the import again, it should update instead i.run() assert len(i.log) == 10 assert i.total == 5 # but importing should not clobber tags if they're unmapped n = deck.getNote(deck.db.scalar("select id from notes")) n.addTag("test") n.flush() i.run() n.load() assert n.tags == ['test'] # if add-only mode, count will be 0 i.importMode = 1 i.run() assert i.total == 0 # and if dupes mode, will reimport everything assert deck.cardCount() == 5 i.importMode = 2 i.run() # includes repeated field assert i.total == 6 assert deck.cardCount() == 11 deck.close()
def test_timing(): d = getEmptyCol() # add a few review cards, due today for i in range(5): f = d.newNote() f['Front'] = "num"+str(i) d.addNote(f) c = f.cards()[0] c.type = 2 c.queue = 2 c.due = 0 c.flush() # fail the first one d.reset() c = d.sched.getCard() # set a a fail delay of 1 second so we don't have to wait d.sched._cardConf(c)['lapse']['delays'][0] = 1/60.0 d.sched.answerCard(c, 1) # the next card should be another review c = d.sched.getCard() assert c.queue == 2 # but if we wait for a second, the failed card should come back time.sleep(1) c = d.sched.getCard() assert c.queue == 1
def test_genrem(): d = getEmptyCol() f = d.newNote() f["Front"] = u"1" f["Back"] = u"" d.addNote(f) assert len(f.cards()) == 1 m = d.models.current() mm = d.models # adding a new template should automatically create cards t = mm.newTemplate("rev") t["qfmt"] = "{{Front}}" t["afmt"] = "" mm.addTemplate(m, t) mm.save(m, templates=True) assert len(f.cards()) == 2 # if the template is changed to remove cards, they'll be removed t["qfmt"] = "{{Back}}" mm.save(m, templates=True) d.remCards(d.emptyCids()) assert len(f.cards()) == 1 # if we add to the note, a card should be automatically generated f.load() f["Back"] = "1" f.flush() assert len(f.cards()) == 2
def test_op(): d = getEmptyCol() # should have no undo by default assert not d.undoName() # let's adjust a study option d.save("studyopts") d.conf["abc"] = 5 # it should be listed as undoable assert d.undoName() == "studyopts" # with about 5 minutes until it's clobbered assert time.time() - d._lastSave < 1 # undoing should restore the old value d.undo() assert not d.undoName() assert "abc" not in d.conf # an (auto)save will clear the undo d.save("foo") assert d.undoName() == "foo" d.save() assert not d.undoName() # and a review will, too d.save("add") f = d.newNote() f["Front"] = "one" d.addNote(f) d.reset() assert d.undoName() == "add" c = d.sched.getCard() d.sched.answerCard(c, 2) assert d.undoName() == "Review"
def setup_basic(): global deck1, deck2, client, server deck1 = getEmptyCol() # add a note to deck 1 f = deck1.newNote() f["Front"] = u"foo" f["Back"] = u"bar" f.tags = [u"foo"] deck1.addNote(f) # answer it deck1.reset() deck1.sched.answerCard(deck1.sched.getCard(), 4) # repeat for deck2 deck2 = getEmptyDeckWith(server=True) f = deck2.newNote() f["Front"] = u"bar" f["Back"] = u"bar" f.tags = [u"bar"] deck2.addNote(f) deck2.reset() deck2.sched.answerCard(deck2.sched.getCard(), 4) # start with same schema and sync time deck1.scm = deck2.scm = 0 # and same mod time, so sync does nothing t = intTime(1000) deck1.save(mod=t) deck2.save(mod=t) server = LocalServer(deck2) client = Syncer(deck1, server)
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_anki2_mediadupes(): tmp = getEmptyCol() # add a note that references a sound n = tmp.newNote() n['Front'] = "[sound:foo.mp3]" mid = n.model()['id'] tmp.addNote(n) # add that sound to media folder with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f: f.write("foo") tmp.close() # it should be imported correctly into an empty deck empty = getEmptyCol() imp = Anki2Importer(empty, tmp.path) imp.run() assert os.listdir(empty.media.dir()) == ["foo.mp3"] # and importing again will not duplicate, as the file content matches empty.remCards(empty.db.list("select id from cards")) imp = Anki2Importer(empty, tmp.path) imp.run() assert os.listdir(empty.media.dir()) == ["foo.mp3"] n = empty.getNote(empty.db.scalar("select id from notes")) assert "foo.mp3" in n.fields[0] # if the local file content is different, and import should trigger a # rename empty.remCards(empty.db.list("select id from cards")) with open(os.path.join(empty.media.dir(), "foo.mp3"), "w") as f: f.write("bar") imp = Anki2Importer(empty, tmp.path) imp.run() assert sorted(os.listdir(empty.media.dir())) == [ "foo.mp3", "foo_%s.mp3" % mid] n = empty.getNote(empty.db.scalar("select id from notes")) assert "_" in n.fields[0] # if the localized media file already exists, we rewrite the note and # media empty.remCards(empty.db.list("select id from cards")) with open(os.path.join(empty.media.dir(), "foo.mp3"), "w") as f: f.write("bar") imp = Anki2Importer(empty, tmp.path) imp.run() assert sorted(os.listdir(empty.media.dir())) == [ "foo.mp3", "foo_%s.mp3" % mid] assert sorted(os.listdir(empty.media.dir())) == [ "foo.mp3", "foo_%s.mp3" % mid] n = empty.getNote(empty.db.scalar("select id from notes")) assert "_" in n.fields[0]
def test_deckTree(): d = getEmptyCol() d.decks.id("new::b::c") d.decks.id("new2") # new should not appear twice in tree names = [x[0] for x in d.sched.deckDueTree()] names.remove("new") assert "new" not in names
def test_suspended(): # create a new empty deck dst = getEmptyCol() # import the 1 card version of the model tmp = getUpgradeDeckPath("suspended12.anki") imp = Anki1Importer(dst, tmp) imp.run() assert dst.db.scalar("select due from cards") < 0
def test_misc(): d = getEmptyCol() f = d.newNote() f['Front'] = '1' f['Back'] = '2' d.addNote(f) c = f.cards()[0] id = d.models.current()['id'] assert c.template()['ord'] == 0
def test_text(): d = getEmptyCol() m = d.models.current() m['tmpls'][0]['qfmt'] = "{{text:Front}}" d.models.save(m) f = d.newNote() f['Front'] = 'hello<b>world' d.addNote(f) assert "helloworld" in f.cards()[0].q()
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_mnemo(): deck = getEmptyCol() file = str(os.path.join(testDir, "support/mnemo.db")) i = MnemosyneImporter(deck, file) i.run() assert deck.cardCount() == 7 assert "a_longer_tag" in deck.tags.all() assert deck.db.scalar("select count() from cards where type = 0") == 1 deck.close()
def test_learn_day(): d = getEmptyCol() # add a note f = d.newNote() f['Front'] = u"one" f = d.addNote(f) d.sched.reset() c = d.sched.getCard() d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880] # pass it d.sched.answerCard(c, 2) # two reps to graduate, 1 more today assert c.left%1000 == 3 assert c.left/1000 == 1 assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() ni = d.sched.nextIvl assert ni(c, 2) == 86400 # answering it will place it in queue 3 d.sched.answerCard(c, 2) assert c.due == d.sched.today+1 assert c.queue == 3 assert not d.sched.getCard() # for testing, move it back a day c.due -= 1 c.flush() d.reset() assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() # nextIvl should work assert ni(c, 2) == 86400*2 # if we fail it, it should be back in the correct queue d.sched.answerCard(c, 1) assert c.queue == 1 d.undo() d.reset() c = d.sched.getCard() d.sched.answerCard(c, 2) # simulate the passing of another two days c.due -= 2 c.flush() d.reset() # the last pass should graduate it into a review card assert ni(c, 2) == 86400 d.sched.answerCard(c, 2) assert c.queue == c.type == 2 # if the lapse step is tomorrow, failing it should handle the counts # correctly c.due = 0 c.flush() d.reset() assert d.sched.counts() == (0, 0, 1) d.sched._cardConf(c)['lapse']['delays'] = [1440] c = d.sched.getCard() d.sched.answerCard(c, 1) assert c.queue == 3 assert d.sched.counts() == (0, 0, 0)
def test_misc(): d = getEmptyCol() f = d.newNote() f["Front"] = u"1" f["Back"] = u"2" d.addNote(f) c = f.cards()[0] id = d.models.current()["id"] assert c.template()["ord"] == 0
def test_nextIvl(): d = getEmptyCol() f = d.newNote() f['Front'] = u"one"; f['Back'] = u"two" d.addNote(f) d.reset() conf = d.decks.confForDid(1) conf['new']['delays'] = [0.5, 3, 10] conf['lapse']['delays'] = [1, 5, 9] c = d.sched.getCard() # new cards ################################################## ni = d.sched.nextIvl assert ni(c, 1) == 30 assert ni(c, 2) == 180 assert ni(c, 3) == 4*86400 d.sched.answerCard(c, 1) # cards in learning ################################################## assert ni(c, 1) == 30 assert ni(c, 2) == 180 assert ni(c, 3) == 4*86400 d.sched.answerCard(c, 2) assert ni(c, 1) == 30 assert ni(c, 2) == 600 assert ni(c, 3) == 4*86400 d.sched.answerCard(c, 2) # normal graduation is tomorrow assert ni(c, 2) == 1*86400 assert ni(c, 3) == 4*86400 # lapsed cards ################################################## c.type = 2 c.ivl = 100 c.factor = 2500 assert ni(c, 1) == 60 assert ni(c, 2) == 100*86400 assert ni(c, 3) == 100*86400 # review cards ################################################## c.queue = 2 c.ivl = 100 c.factor = 2500 # failing it should put it at 60s assert ni(c, 1) == 60 # or 1 day if relearn is false d.sched._cardConf(c)['lapse']['delays']=[] assert ni(c, 1) == 1*86400 # (* 100 1.2 86400)10368000.0 assert ni(c, 2) == 10368000 # (* 100 2.5 86400)21600000.0 assert ni(c, 3) == 21600000 # (* 100 2.5 1.3 86400)28080000.0 assert ni(c, 4) == 28080000 assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_preview(): # add cards d = getEmptyCol() f = d.newNote() f['Front'] = "one" d.addNote(f) c = f.cards()[0] orig = copy.copy(c) f2 = d.newNote() f2['Front'] = "two" d.addNote(f2) # cram deck did = d.decks.newDyn("Cram") cram = d.decks.get(did) cram['resched'] = False d.sched.rebuildDyn(did) d.reset() # grab the first card c = d.sched.getCard() assert d.sched.answerButtons(c) == 2 assert d.sched.nextIvl(c, 1) == 600 assert d.sched.nextIvl(c, 2) == 0 # failing it will push its due time back due = c.due d.sched.answerCard(c, 1) assert c.due != due # the other card should come next c2 = d.sched.getCard() assert c2.id != c.id # passing it will remove it d.sched.answerCard(c2, 2) assert c2.queue == 0 assert c2.reps == 0 assert c2.type == 0 # the other card should appear again c = d.sched.getCard() assert c.id == orig.id # emptying the filtered deck should restore card d.sched.emptyDyn(did) c.load() assert c.queue == 0 assert c.reps == 0 assert c.type == 0
def test_cloze(): col = getEmptyCol() m = col.models.by_name("Cloze") note = col.new_note(m) 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 note = col.new_note(m) note["Text"] = "hello {{c1::world}}" assert col.addNote(note) == 1 assert "hello <span class=cloze>[...]</span>" in note.cards()[0].question() assert "hello <span class=cloze>world</span>" in note.cards()[0].answer() # and with a comment note = col.new_note(m) note["Text"] = "hello {{c1::world::typical}}" assert col.addNote(note) == 1 assert "<span class=cloze>[typical]</span>" in note.cards()[0].question() assert "<span class=cloze>world</span>" in note.cards()[0].answer() # and with 2 clozes note = col.new_note(m) note["Text"] = "hello {{c1::world}} {{c2::bar}}" assert col.addNote(note) == 2 (c1, c2) = note.cards() assert "<span class=cloze>[...]</span> bar" in c1.question() assert "<span class=cloze>world</span> bar" in c1.answer() assert "world <span class=cloze>[...]</span>" in c2.question() assert "world <span class=cloze>bar</span>" in c2.answer() # if there are multiple answers for a single cloze, they are given in a # list note = col.new_note(m) note["Text"] = "a {{c1::b}} {{c1::c}}" assert col.addNote(note) == 1 assert "<span class=cloze>b</span> <span class=cloze>c</span>" in ( note.cards()[0].answer() ) # if we add another cloze, a card should be generated cnt = col.card_count() note["Text"] = "{{c2::hello}} {{c1::foo}}" note.flush() 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
def test_suspend(): d = getEmptyCol() f = d.newNote() f['Front'] = "one" d.addNote(f) c = f.cards()[0] # suspending d.reset() assert d.sched.getCard() d.sched.suspendCards([c.id]) d.reset() assert not d.sched.getCard() # unsuspending d.sched.unsuspendCards([c.id]) d.reset() assert d.sched.getCard() # should cope with rev cards being relearnt c.due = 0 c.ivl = 100 c.type = 2 c.queue = 2 c.flush() d.reset() c = d.sched.getCard() d.sched.answerCard(c, 1) assert c.due >= time.time() assert c.queue == 1 assert c.type == 2 d.sched.suspendCards([c.id]) d.sched.unsuspendCards([c.id]) c.load() assert c.queue == 2 assert c.type == 2 assert c.due == 1 # should cope with cards in cram decks c.due = 1 c.flush() cram = d.decks.newDyn("tmp") d.sched.rebuildDyn() c.load() assert c.due != 1 assert c.did != 1 d.sched.suspendCards([c.id]) c.load() assert c.due == 1 assert c.did == 1
def test_flatten(): # ARRANGE col = getEmptyCol() parent_id = col.decks.id("deck1") child1_id = col.decks.id("deck1::child1") child2_id = col.decks.id("deck1::child2") note1 = create_note(col, child1_id) note2 = create_note(col, child2_id) # ACT col.decks.flatten(parent_id) note1.load() note2.load() # ASSERT assert note1.cards()[0].did == parent_id assert note2.cards()[0].did == parent_id
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(m) f['name'] = "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(m) f['name'] = "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_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 f = m["flds"][2] 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(): col = getEmptyCol() m = col.models.current() mm = col.models t = mm.new_template("Reverse") t["qfmt"] = "{{Back}}" t["afmt"] = "{{Front}}" mm.add_template(m, t) mm.save(m) note = col.newNote() note["Front"] = "1" note["Back"] = "2" col.addNote(note) 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 col.models.reposition_template(m, c.template(), 1) col.models.update(m) c.load() c2.load() assert c.ord == 1 assert c2.ord == 0 # removing a template should delete its cards col.models.remove_template(m, m["tmpls"][0]) col.models.update(m) assert col.card_count() == 1 # and should have updated the other cards' ordinals c = note.cards()[0] assert c.ord == 0 assert strip_html(c.question()) == "1" # it shouldn't be possible to orphan notes by removing templates t = mm.new_template("template name") t["qfmt"] = "{{Front}}2" 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 )
def test_renameForDragAndDrop(): d = getEmptyCol() def deckNames(): return [ name for name in sorted(d.decks.allNames()) if name != 'Default' ] languages_did = d.decks.id('Languages') chinese_did = d.decks.id('Chinese') hsk_did = d.decks.id('Chinese::HSK') # Renaming also renames children d.decks.renameForDragAndDrop(chinese_did, languages_did) assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] # Dragging a deck onto itself is a no-op d.decks.renameForDragAndDrop(languages_did, languages_did) assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] # Dragging a deck onto its parent is a no-op d.decks.renameForDragAndDrop(hsk_did, chinese_did) assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] # Dragging a deck onto a descendant is a no-op d.decks.renameForDragAndDrop(languages_did, hsk_did) assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] # Can drag a grandchild onto its grandparent. It becomes a child d.decks.renameForDragAndDrop(hsk_did, languages_did) assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::HSK' ] # Can drag a deck onto its sibling d.decks.renameForDragAndDrop(hsk_did, chinese_did) assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] # Can drag a deck back to the top level d.decks.renameForDragAndDrop(chinese_did, None) assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] # Dragging a top level deck to the top level is a no-op d.decks.renameForDragAndDrop(chinese_did, None) assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] # '' is a convenient alias for the top level DID d.decks.renameForDragAndDrop(hsk_did, '') assert deckNames() == [ 'Chinese', 'HSK', 'Languages' ]
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.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 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"]
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 <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() 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() 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() f['Text'] = "a {{c1::b}} {{c1::c}}" assert d.addNote(f) == 1 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() 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_finished(): d = getEmptyCol() # nothing due assert "Congratulations" in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg() f = d.newNote() f['Front'] = u"one"; f['Back'] = u"two" d.addNote(f) # have a new card assert "new cards available" in d.sched.finishedMsg() # turn it into a review d.reset() c = f.cards()[0] c.startTimer() d.sched.answerCard(c, 3) # nothing should be due tomorrow, as it's due in a week assert "Congratulations" in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
def test_apkg(): tmp = getEmptyCol() apkg = unicode(os.path.join(testDir, "support/media.apkg")) imp = AnkiPackageImporter(tmp, apkg) assert os.listdir(tmp.media.dir()) == [] imp.run() assert os.listdir(tmp.media.dir()) == ['foo.wav'] # importing again should be idempotent in terms of media tmp.remCards(tmp.db.list("select id from cards")) imp = AnkiPackageImporter(tmp, apkg) imp.run() assert os.listdir(tmp.media.dir()) == ['foo.wav'] # but if the local file has different data, it will rename tmp.remCards(tmp.db.list("select id from cards")) open(os.path.join(tmp.media.dir(), "foo.wav"), "w").write("xyz") imp = AnkiPackageImporter(tmp, apkg) imp.run() assert len(os.listdir(tmp.media.dir())) == 2
def test_addDelTags(): deck = getEmptyCol() f = deck.newNote() f['Front'] = "1" deck.addNote(f) f2 = deck.newNote() f2['Front'] = "2" deck.addNote(f2) # adding for a given id deck.tags.bulkAdd([f.id], "foo") f.load(); f2.load() assert "foo" in f.tags assert "foo" not in f2.tags # should be canonified deck.tags.bulkAdd([f.id], "foo aaa") f.load() assert f.tags[0] == "aaa" assert len(f.tags) == 2
def test_findReplace(): col = getEmptyCol() note = col.newNote() note["Front"] = "foo" note["Back"] = "bar" col.addNote(note) note2 = col.newNote() note2["Front"] = "baz" note2["Back"] = "foo" col.addNote(note2) nids = [note.id, note2.id] # should do nothing assert (col.find_and_replace(note_ids=nids, search="abc", replacement="123").count == 0) # global replace assert (col.find_and_replace(note_ids=nids, search="foo", replacement="qux").count == 2) note.load() assert note["Front"] == "qux" note2.load() assert note2["Back"] == "qux" # single field replace assert (col.find_and_replace(note_ids=nids, search="qux", replacement="foo", field_name="Front").count == 1) note.load() assert note["Front"] == "foo" note2.load() assert note2["Back"] == "qux" # regex replace assert (col.find_and_replace(note_ids=nids, search="B.r", replacement="reg").count == 0) note.load() assert note["Back"] != "reg" assert (col.find_and_replace(note_ids=nids, search="B.r", replacement="reg", regex=True).count == 1) note.load() assert note["Back"] == "reg"
def test_rename(): d = getEmptyCol() id = d.decks.id("hello::world") # should be able to rename into a completely different branch, creating # parents as necessary d.decks.rename(d.decks.get(id), "foo::bar") assert "foo" in d.decks.allNames() assert "foo::bar" in d.decks.allNames() assert "hello::world" not in d.decks.allNames() # create another deck id = d.decks.id("tmp") # we can't rename it if it conflicts assertException(Exception, lambda: d.decks.rename(d.decks.get(id), "foo")) # when renaming, the children should be renamed too d.decks.id("one::two::three") id = d.decks.id("one") d.decks.rename(d.decks.get(id), "yo") for n in "yo", "yo::two", "yo::two::three": assert n in d.decks.allNames()
def test_addDelTags(): col = getEmptyCol() note = col.newNote() note["Front"] = "1" col.addNote(note) note2 = col.newNote() note2["Front"] = "2" col.addNote(note2) # adding for a given id col.tags.bulk_add([note.id], "foo") note.load() note2.load() assert "foo" in note.tags assert "foo" not in note2.tags # should be canonified col.tags.bulk_add([note.id], "foo aaa") note.load() assert note.tags[0] == "aaa" assert len(note.tags) == 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( '\(a\) <span class=cloze>[...]</span> \[ [...] \]')
def test_apkg(): col = getEmptyCol() apkg = str(os.path.join(testDir, "support", "media.apkg")) imp = AnkiPackageImporter(col, apkg) assert os.listdir(col.media.dir()) == [] imp.run() assert os.listdir(col.media.dir()) == ["foo.wav"] # importing again should be idempotent in terms of media col.remove_cards_and_orphaned_notes(col.db.list("select id from cards")) imp = AnkiPackageImporter(col, apkg) imp.run() assert os.listdir(col.media.dir()) == ["foo.wav"] # but if the local file has different data, it will rename col.remove_cards_and_orphaned_notes(col.db.list("select id from cards")) with open(os.path.join(col.media.dir(), "foo.wav"), "w") as note: note.write("xyz") imp = AnkiPackageImporter(col, apkg) imp.run() assert len(os.listdir(col.media.dir())) == 2
def test_previewCards(): deck = getEmptyCol() f = deck.newNote() f["Front"] = "1" f["Back"] = "2" # non-empty and active cards = deck.previewCards(f, 0) assert len(cards) == 1 assert cards[0].ord == 0 # all templates cards = deck.previewCards(f, 2) assert len(cards) == 1 # add the note, and test existing preview deck.addNote(f) cards = deck.previewCards(f, 1) assert len(cards) == 1 assert cards[0].ord == 0 # make sure we haven't accidentally added cards to the db assert deck.cardCount() == 1
def test_button_spacing(): d = getEmptyCol() f = d.newNote() f['Front'] = u"one" d.addNote(f) # 1 day ivl review card due now c = f.cards()[0] c.type = 2 c.queue = 2 c.due = d.sched.today c.reps = 1 c.ivl = 1 c.startTimer() c.flush() d.reset() ni = d.sched.nextIvlStr assert ni(c, 2) == "2 days" assert ni(c, 3) == "3 days" assert ni(c, 4) == "4 days"
def setup(): # create collection 1 with a single note c1 = getEmptyCol() f = c1.newNote() f['Front'] = "startingpoint" nid = f.id c1.addNote(f) cid = f.cards()[0].id c1.beforeUpload() # start both clients and server off in this state s1path = c1.path.replace(".anki2", "-s1.anki2") c2path = c1.path.replace(".anki2", "-c2.anki2") shutil.copy2(c1.path, s1path) shutil.copy2(c1.path, c2path) # open them c1 = Collection(c1.path) c2 = Collection(c2path) s1 = Collection(s1path, server=True) return c1, c2, s1, nid, cid
def test_cram_rem(): d = getEmptyCol() f = d.newNote() f['Front'] = u"one" d.addNote(f) oldDue = f.cards()[0].due did = d.decks.newDyn("Cram") d.sched.rebuildDyn(did) d.reset() c = d.sched.getCard() d.sched.answerCard(c, 2) # answering the card will put it in the learning queue assert c.type == c.queue == 1 assert c.due != oldDue # if we terminate cramming prematurely it should be set back to new d.sched.emptyDyn(did) c.load() assert c.type == c.queue == 0 assert c.due == oldDue
def test_noteAddDelete(): deck = getEmptyCol() # add a note f = deck.newNote() f["Front"] = "one" f["Back"] = "two" n = deck.addNote(f) assert n == 1 # test multiple cards - add another template m = deck.models.current() mm = deck.models t = mm.newTemplate("Reverse") t["qfmt"] = "{{Back}}" t["afmt"] = "{{Front}}" mm.addTemplate(m, t) mm.save(m) # the default save doesn't generate cards assert deck.cardCount() == 1 # but when templates are edited such as in the card layout screen, it # should generate cards on close mm.save(m, templates=True, updateReqs=False) assert deck.cardCount() == 2 # creating new notes should use both cards f = deck.newNote() f["Front"] = "three" f["Back"] = "four" n = deck.addNote(f) assert n == 2 assert deck.cardCount() == 4 # check q/a generation c0 = f.cards()[0] assert "three" in c0.q() # it should not be a duplicate assert not f.dupeOrEmpty() # now let's make a duplicate f2 = deck.newNote() f2["Front"] = "one" f2["Back"] = "" assert f2.dupeOrEmpty() # empty first field should not be permitted either f2["Front"] = " " assert f2.dupeOrEmpty()
def test_relearn_no_steps(): d = getEmptyCol() f = d.newNote() f['Front'] = "one" d.addNote(f) c = f.cards()[0] c.ivl = 100 c.due = d.sched.today c.type = c.queue = 2 c.flush() conf = d.decks.confForDid(1) conf['lapse']['delays'] = [] d.decks.save(conf) # fail the card d.reset() c = d.sched.getCard() d.sched.answerCard(c, 1) assert c.type == c.queue == 2
def test_cloze_mathjax(): col = getEmptyCol() m = col.models.by_name("Cloze") note = col.new_note(m) 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].question() assert "class=cloze" in note.cards()[1].question() assert "class=cloze" not in note.cards()[2].question() assert "class=cloze" in note.cards()[3].question() assert "class=cloze" in note.cards()[4].question() note = col.new_note(m) note["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]" assert col.addNote(note) assert len(note.cards()) == 1 assert (note.cards()[0].question().endswith( r"\(a\) <span class=cloze>[...]</span> \[ [...] \]"))
def test_anki2_diffmodel_templates(): # different from the above as this one tests only the template text being # changed, not the number of cards/fields dst = getEmptyCol() # import the first version of the model tmp = getUpgradeDeckPath("diffmodeltemplates-1.apkg") imp = AnkiPackageImporter(dst, tmp) imp.dupeOnSchemaChange = True imp.run() # then the version with updated template tmp = getUpgradeDeckPath("diffmodeltemplates-2.apkg") imp = AnkiPackageImporter(dst, tmp) imp.dupeOnSchemaChange = True imp.run() # collection should contain the note we imported assert dst.noteCount() == 1 # the front template should contain the text added in the 2nd package tcid = dst.findCards("")[0] # only 1 note in collection tnote = dst.getCard(tcid).note() assert "Changed Front Template" in tnote.cards()[0].template()["qfmt"]
def test_furigana(): deck = getEmptyCol() mm = deck.models m = mm.current() # filter should work m["tmpls"][0]["qfmt"] = "{{kana:Front}}" mm.save(m) n = deck.newNote() n["Front"] = "foo[abc]" deck.addNote(n) c = n.cards()[0] assert c.q().endswith("abc") # and should avoid sound n["Front"] = "foo[sound:abc.mp3]" n.flush() assert "anki:play" in c.q(reload=True) # it shouldn't throw an error while people are editing m["tmpls"][0]["qfmt"] = "{{kana:}}" mm.save(m) c.q(reload=True)
def test_norelearn(): d = getEmptyCol() # add a note f = d.newNote() f['Front'] = u"one" d.addNote(f) c = f.cards()[0] c.type = 2 c.queue = 2 c.due = 0 c.factor = 2500 c.reps = 3 c.lapses = 1 c.ivl = 100 c.startTimer() c.flush() d.reset() d.sched.answerCard(c, 1) d.sched._cardConf(c)['lapse']['delays'] = [] d.sched.answerCard(c, 1)
def test_furigana(): deck = getEmptyCol() mm = deck.models m = mm.current() # filter should work m['tmpls'][0]['qfmt'] = '{{kana:Front}}' mm.save(m) n = deck.newNote() n['Front'] = 'foo[abc]' deck.addNote(n) c = n.cards()[0] assert c.q().endswith("abc") # and should avoid sound n['Front'] = 'foo[sound:abc.mp3]' n.flush() assert "sound:" in c.q(reload=True) # it shouldn't throw an error while people are editing m['tmpls'][0]['qfmt'] = '{{kana:}}' mm.save(m) c.q(reload=True)
def test_filt_keep_lrn_state(): d = getEmptyCol() f = d.newNote() f['Front'] = "one" d.addNote(f) # fail the card outside filtered deck c = d.sched.getCard() d.sched._cardConf(c)['new']['delays'] = [1, 10, 61] d.decks.save() d.sched.answerCard(c, 1) assert c.type == c.queue == 1 assert c.left == 3003 d.sched.answerCard(c, 3) assert c.type == c.queue == 1 # create a dynamic deck and refresh it did = d.decks.newDyn("Cram") d.sched.rebuildDyn(did) d.reset() # card should still be in learning state c.load() assert c.type == c.queue == 1 assert c.left == 2002 # should be able to advance learning steps d.sched.answerCard(c, 3) # should be due at least an hour in the future assert c.due - intTime() > 60 * 60 # emptying the deck preserves learning state d.sched.emptyDyn(did) c.load() assert c.type == c.queue == 1 assert c.left == 1001 assert c.due - intTime() > 60 * 60