Example #1
0
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
Example #2
0
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)
Example #3
0
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())
Example #4
0
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)
Example #5
0
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")
Example #6
0
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
Example #7
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")
Example #8
0
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
Example #9
0
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") == []
Example #10
0
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"
Example #11
0
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])
Example #12
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
Example #13
0
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") == []
Example #14
0
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
Example #15
0
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()
Example #16
0
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
Example #17
0
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
Example #18
0
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"
Example #19
0
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)
Example #20
0
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]
Example #21
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]
Example #22
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
Example #23
0
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
Example #24
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
Example #25
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()
Example #26
0
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
Example #27
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()
Example #28
0
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)
Example #29
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
Example #30
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"
Example #31
0
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
Example #32
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
Example #33
0
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
Example #34
0
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
Example #35
0
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"]
Example #36
0
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"]
Example #37
0
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
    )
Example #38
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' ]
Example #39
0
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"]
Example #40
0
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
Example #41
0
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()
Example #42
0
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
Example #43
0
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
Example #44
0
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"
Example #45
0
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()
Example #46
0
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
Example #47
0
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> \[ [...] \]')
Example #48
0
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
Example #49
0
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
Example #50
0
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"
Example #51
0
 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
Example #52
0
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
Example #53
0
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()
Example #54
0
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
Example #55
0
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> \[ [...] \]"))
Example #56
0
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"]
Example #57
0
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)
Example #58
0
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)
Example #60
0
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