From 870c80e076ed0f2dea44dd3831391df8bfb53ab3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 13 Mar 2011 18:32:03 +0900 Subject: [PATCH] add genCards(), previewCards(), and more unit tests --- anki/cards.py | 2 +- anki/deck.py | 149 +++++++++++++++++++++----------------------- anki/facts.py | 3 + anki/models.py | 7 ++- tests/test_cards.py | 82 ++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 80 deletions(-) create mode 100644 tests/test_cards.py diff --git a/anki/cards.py b/anki/cards.py index da7020a6d..2e6858871 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -112,7 +112,7 @@ streak=?, lapses=?, grade=?, cycles=? where id = ?""", return self.deck.getFact(self.fid) def template(self): - return self.deck.getTemplate(self.tid) + return self.deck.getTemplate(self.fact().mid, self.ord) def startTimer(self): self.timerStarted = time.time() diff --git a/anki/deck.py b/anki/deck.py index b24de35e8..3b3990841 100644 --- a/anki/deck.py +++ b/anki/deck.py @@ -156,9 +156,8 @@ qconf=?, conf=?, data=?""", def getFact(self, id): return anki.facts.Fact(self, id=id) - def getTemplate(self, id): - return anki.models.Template(self, self.deck.db.first( - "select * from templates where id = ?", id)) + def getTemplate(self, mid, ord): + return self.getModel(mid).templates[ord] def getModel(self, mid): return anki.models.Model(self, mid) @@ -198,27 +197,37 @@ qconf=?, conf=?, data=?""", # notice any new tags self.registerTags(fact.tags) # if random mode, determine insertion point - isRandom = self.qconf['newCardOrder'] == NEW_CARDS_RANDOM - if isRandom: + if self.qconf['newCardOrder'] == NEW_CARDS_RANDOM: due = random.randrange(0, 1000000) + else: + due = fact.id # add cards ncards = 0 for template in cms: - card = anki.cards.Card(self) - card.id = self.nextID("cid") - card.fid = fact.id - card.ord = template['ord'] - card.gid = template['gid'] or gid - if isRandom: - card.due = due - else: - card.due = fact.id - card.flush() + self._newCard(fact, template, due, gid) ncards += 1 return ncards + def _deleteFacts(self, ids): + "Bulk delete facts by ID. Don't call this directly." + if not ids: + return + strids = ids2str(ids) + self.db.execute("delete from facts where id in %s" % strids) + self.db.execute("delete from fsums where fid in %s" % strids) + + def _deleteDanglingFacts(self): + "Delete any facts without cards. Don't call this directly." + ids = self.db.list(""" +select id from facts where id not in (select distinct fid from cards)""") + self._deleteFacts(ids) + return ids + + # Card creation + ########################################################################## + def findTemplates(self, fact, checkActive=True): - "Return active, non-empty templates." + "Return (active), non-empty templates." ok = [] for template in fact.model.templates: if template['actv'] or not checkActive: @@ -236,67 +245,57 @@ qconf=?, conf=?, data=?""", ok.append(template) return ok - def genCards(self, fact, templates): - "Generate cards for templates if cards not empty." - # templates should have .ord set - ids = [] - for template in self.findTemplates(fact, False): + def genCards(self, fact, templates, gid): + "Generate cards for templates if cards not empty. Return cards." + cards = [] + # if random mode, determine insertion point + if self.qconf['newCardOrder'] == NEW_CARDS_RANDOM: + # if this fact has existing new cards, use their due time + due = self.db.scalar( + "select due from cards where fid = ? and queue = 2", fact.id) + due = due or random.randrange(1, 1000000) + else: + due = fact.id + for template in self.findTemplates(fact, checkActive=False): if template not in templates: continue + # if it doesn't already exist if not self.db.scalar( "select 1 from cards where fid = ? and ord = ?", - fact.id, template.ord): - card = anki.cards.Card( - fact, template, - fact.created+0.0001*template.ord) - raise Exception("incorrect; not checking selective study") - self.newAvail += 1 - ids.append(card.id) - - if ids: - fact.setMod(textChanged=True, deck=self) - self.setMod() - return ids - - def _deleteFacts(self, ids): - "Bulk delete facts by ID. Don't call this directly." - if not ids: - return - strids = ids2str(ids) - self.db.execute("delete from facts where id in %s" % strids) - self.db.execute("delete from fsums where fid in %s" % strids) - - def _deleteDanglingFacts(self): - "Delete any facts without cards. Don't call this directly." - ids = self.db.list(""" -select id from facts where id not in (select distinct fid from cards)""") - self._deleteFacts(ids) - return ids - - def previewFact(self, oldFact, cms=None): - "Duplicate fact and generate cards for preview. Don't add to deck." - # check we have card models available - if cms is None: - cms = self.findTemplates(oldFact, checkActive=True) - if not cms: - return [] - fact = self.cloneFact(oldFact) - # proceed - cards = [] - for template in cms: - card = anki.cards.Card(fact, template) - cards.append(card) - fact.setMod(textChanged=True, deck=self, media=False) + fact.id, template['ord']): + # create + cards.append(self._newCard(fact, template, due, gid)) return cards - def cloneFact(self, oldFact): - "Copy fact into new session." - model = self.db.query(Model).get(oldFact.model.id) - fact = self.newFact(model) - for field in fact.fdata: - fact[field.name] = oldFact[field.name] - fact._tags = oldFact._tags - return fact + # type 0 - when previewing in add dialog, only non-empty & active + # type 1 - when previewing edit, only existing + # type 2 - when previewing in models dialog, all + def previewCards(self, fact, type=0): + "Return uncommited cards for preview." + if type == 0: + cms = self.findTemplates(fact, checkActive=True) + elif type == 1: + cms = [c.template() for c in fact.cards()] + else: + cms = fact.model.templates + if not cms: + return [] + cards = [] + for template in cms: + cards.append(self._newCard(fact, template, 1, 1, flush=False)) + return cards + + def _newCard(self, fact, template, due, gid, flush=True): + "Create a new card." + card = anki.cards.Card(self) + card.id = self.nextID("cid") + card.fid = fact.id + card.ord = template['ord'] + card.gid = template['gid'] or gid + card.due = due + if flush: + card.flush() + return card # Cards ########################################################################## @@ -325,7 +324,7 @@ select id from facts where id not in (select distinct fid from cards)""") self.db.list("select fid from cards where id in "+sids)) # need to handle delete of fsums/revlog remotely after sync self.db.execute( - "update cards set crt = 0, mod = ? where id in "+sids, + "update cards set crt = 0, queue = -4, mod = ? where id in "+sids, intTime()) self.db.execute( "update facts set crt = 0, mod = ? where id in "+sfids, @@ -440,12 +439,6 @@ select id from cards where fid in (select id from facts where mid = ?)""", self.conf['currentModelId'] = self.db.scalar( "select id from models limit 1") - def modelUseCount(self, model): - "Return number of facts using model." - return self.db.scalar("select count() from facts " - "where facts.mid = :id", - id=model.id) - # Field checksums and sorting fields ########################################################################## diff --git a/anki/facts.py b/anki/facts.py index 9d41325bc..2e719980d 100644 --- a/anki/facts.py +++ b/anki/facts.py @@ -69,6 +69,9 @@ insert or replace into facts values (?, ?, ?, ?, ?, ?, ?, ?)""", return [self.deck.getCard(id) for id in self.deck.db.list( "select id from cards where fid = ? order by id", self.id)] + def model(self): + return self.deck.getModel(self.mid) + # Dict interface ################################################## diff --git a/anki/models.py b/anki/models.py index 1d8f0e1c9..96530b581 100644 --- a/anki/models.py +++ b/anki/models.py @@ -80,7 +80,12 @@ insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""", self.id = ret.lastrowid def fids(self): - return self.deck.db.list("select id from facts where mid = ?", self.id) + return self.deck.db.list( + "select id from facts where mid = ?", self.id) + + def useCount(self): + return self.deck.db.scalar( + "select count() from facts where mid = ?", self.id) # Copying ################################################## diff --git a/tests/test_cards.py b/tests/test_cards.py new file mode 100644 index 000000000..8fd239f5a --- /dev/null +++ b/tests/test_cards.py @@ -0,0 +1,82 @@ +# coding: utf-8 + +from anki.consts import * +from tests.shared import getEmptyDeck + +def test_genCards(): + deck = getEmptyDeck() + f = deck.newFact() + f['Front'] = u'1' + f['Back'] = u'2' + deck.addFact(f) + cards = deck.genCards(f, f.model.templates, 1) + assert len(cards) == 1 + assert cards[0].ord == 1 + assert deck.cardCount() == 2 + assert cards[0].due == f.id + # should work on random mode too + deck.qconf['newCardOrder'] = NEW_CARDS_RANDOM + f = deck.newFact() + f['Front'] = u'1' + f['Back'] = u'2' + deck.addFact(f) + cards = deck.genCards(f, f.model.templates, 1) + assert deck.cardCount() == 4 + c = deck.db.list("select due from cards where fid = ?", f.id) + assert c[0] == c[1] + +def test_previewCards(): + deck = getEmptyDeck() + f = deck.newFact() + f['Front'] = u'1' + f['Back'] = u'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) == 2 + # add the fact, and test existing preview + deck.addFact(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_delete(): + deck = getEmptyDeck() + f = deck.newFact() + f['Front'] = u'1' + f['Back'] = u'2' + deck.addFact(f) + cid = f.cards()[0].id + # when the schema is dirty, deletion should be immediate + assert deck.schemaDirty() == True + deck.deleteCard(cid) + assert deck.cardCount() == 0 + assert deck.factCount() == 0 + assert deck.db.scalar("select count() from facts") == 0 + assert deck.db.scalar("select count() from cards") == 0 + # add the fact back + deck.addFact(f) + assert deck.cardCount() == 1 + # mark the schema as clean + deck.lastSync = deck.schema + 1 + # cards/facts should go in the deletion log instead + cid = f.cards()[0].id + deck.deleteCard(cid) + assert deck.cardCount() == 0 + assert deck.factCount() == 0 + assert deck.db.scalar("select count() from facts") == 1 + assert deck.db.scalar("select count() from cards") == 1 + assert deck.db.scalar("select 1 from cards where crt = 0") == 1 + assert deck.db.scalar("select 1 from facts where crt = 0") == 1 + assert deck.db.scalar("select queue from cards") == -4 + # modifying the schema should empty the trash + deck.modSchema() + assert deck.cardCount() == 0 + assert deck.factCount() == 0 + assert deck.db.scalar("select count() from facts") == 0 + assert deck.db.scalar("select count() from cards") == 0