diff --git a/anki/sched.py b/anki/sched.py index f9b14559e..c284128a7 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -57,11 +57,10 @@ class Scheduler: def answerCard(self, card, ease): self.col.log() assert 1 <= ease <= 4 - assert 0 <= card.queue <= 3 + assert 0 <= card.queue <= 4 self.col.markReview(card) if self._burySiblingsOnAnswer: self._burySiblings(card) - card.reps += 1 self._answerCard(card, ease) @@ -71,6 +70,11 @@ class Scheduler: card.flushSched() def _answerCard(self, card, ease): + if self._previewingCard(card): + self._answerCardPreview(card, ease) + return + + card.reps += 1 card.wasNew = card.type == 0 if card.queue == 0: @@ -88,6 +92,22 @@ class Scheduler: # update daily limit self._updateStats(card, 'rev') + # hard-coded for now + _previewDelay = 600 + + def _answerCardPreview(self, card, ease): + assert 1 <= ease <= 2 + + if ease == 1: + # repeat after delay + card.queue = 4 + card.due = intTime() + self._previewDelay + self.lrnCount += 1 + else: + # restore original card state and remove from filtered deck + self._restoreFromFiltered(card) + self._removeFromFiltered(card) + def counts(self, card=None): counts = [self.newCount, self.lrnCount, self.revCount] if card: @@ -117,23 +137,14 @@ order by due""" % self._deckLimit(), return ret def countIdx(self, card): - if card.queue == 3: + if card.queue in (3,4): return 1 return card.queue def answerButtons(self, card): conf = self._cardConf(card) - - # fixme: resched=off case - # if card.odid: - # if not conf['resched']: - # if card.queue == 2: - # return 4 - # conf = self._lrnConf(card) - # if card.type in (0,1) or len(conf['delays']) > 1: - # return 3 - # return 2 - + if card.odid and not conf['resched']: + return 2 return 4 def unburyCards(self): @@ -449,6 +460,10 @@ did in %s and queue = 1 and due < ? limit %d)""" % ( select count() from cards where did in %s and queue = 3 and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), self.today) + # previews + self.lrnCount += self.col.db.scalar(""" +select count() from cards where did in %s and queue = 4 +limit %d""" % (self._deckLimit(), self.reportLimit)) def _resetLrn(self): self._resetLrnCount() @@ -464,7 +479,7 @@ and due <= ? limit %d""" % (self._deckLimit(), self.reportLimit), return True self._lrnQueue = self.col.db.all(""" select due, id from cards where -did in %s and queue = 1 and due < :lim +did in %s and queue in (1,4) and due < :lim limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff) # as it arrives sorted by did first, we need to sort it self._lrnQueue.sort() @@ -478,7 +493,10 @@ limit %d""" % (self._deckLimit(), self.reportLimit), lim=self.dayCutoff) if self._lrnQueue[0][0] < cutoff: id = heappop(self._lrnQueue)[1] card = self.col.getCard(id) - self.lrnCount -= card.left // 1000 + if self._previewingCard(card): + self.lrnCount -= 1 + else: + self.lrnCount -= card.left // 1000 return card # daily learning @@ -542,10 +560,9 @@ did = ? and queue = 3 and due <= ? limit ?""", def _moveToFirstStep(self, card, conf): card.left = self._startingLeft(card) - resched = self._resched(card) card.lastIvl = card.ivl - if card.type == 2 and resched: + if card.type == 2: # review card that will move to relearning card.ivl = self._lapseIvl(card, self._lrnConf(card)) @@ -624,27 +641,15 @@ did = ? and queue = 3 and due <= ? limit ?""", def _rescheduleAsRev(self, card, conf, early): lapse = card.type == 2 - resched = self._resched(card) - if resched: - if lapse: - self._rescheduleGraduatingLapse(card) - else: - self._rescheduleNew(card, conf, early) + if lapse: + self._rescheduleGraduatingLapse(card) + else: + self._rescheduleNew(card, conf, early) # if we were dynamic, graduating means moving back to the old deck if card.odid: - # and leaving learning if scheduling off - if not resched: - card.due = card.odue - if card.type == 2: - card.queue = 2 - else: - card.queue = card.type = 0 - - card.did = card.odid - card.odue = 0 - card.odid = 0 + self._removeFromFiltered(card) def _rescheduleGraduatingLapse(self, card): card.due = self.today+card.ivl @@ -860,40 +865,28 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)""" # update interval card.lastIvl = card.ivl - if self._resched(card): - self._updateRevIvl(card, ease) - # then the rest - card.factor = max(1300, card.factor+[-150, 0, 150][ease-2]) - card.due = self.today + card.ivl - else: - card.due = card.odue + self._updateRevIvl(card, ease) + # then the rest + card.factor = max(1300, card.factor+[-150, 0, 150][ease-2]) + card.due = self.today + card.ivl # card leaves filtered deck - if card.odid: - card.did = card.odid - card.odid = 0 - card.odue = 0 + self._removeFromFiltered(card) def _rescheduleEarlyRev(self, card, ease): # update interval card.lastIvl = card.ivl - if self._resched(card): - self._updateEarlyRevIvl(card, ease) - # then the rest - card.factor = max(1300, card.factor+[-150, 0, 150][ease-2]) - card.due = self.today + card.ivl - else: - card.due = card.odue + self._updateEarlyRevIvl(card, ease) + # then the rest + card.factor = max(1300, card.factor+[-150, 0, 150][ease-2]) + card.due = self.today + card.ivl # move from 0->2 card.queue = 2 # card leaves filtered deck - if card.odid: - card.did = card.odid - card.odid = 0 - card.odue = 0 + self._removeFromFiltered(card) def _logRev(self, card, ease, delay, type): def log(): @@ -1111,6 +1104,25 @@ did = ?, due = ?, usn = ? where id = ? """ self.col.db.executemany(query, data) + def _removeFromFiltered(self, card): + if card.odid: + card.did = card.odid + card.odue = 0 + card.odid = 0 + + def _restoreFromFiltered(self, card): + assert card.odid + + card.due = card.odue + + if card.type in (0, 2): + card.queue = card.type + else: + if card.odue > 1000000000: + card.queue = 1 + else: + card.queue = 3 + # Leeches ########################################################################## @@ -1129,12 +1141,6 @@ did = ?, due = ?, usn = ? where id = ? # handle a = conf['leechAction'] if a == 0: - # if it has an old due, remove it from cram/relearning - if card.odue: - card.due = card.odue - if card.odid: - card.did = card.odid - card.odue = card.odid = 0 card.queue = -1 # notify UI runHook("leech", card) @@ -1194,11 +1200,9 @@ did = ?, due = ?, usn = ? where id = ? def _deckLimit(self): return ids2str(self.col.decks.active()) - def _resched(self, card): + def _previewingCard(self, card): conf = self._cardConf(card) - if not conf['dyn']: - return True - return conf['resched'] + return conf['dyn'] and not conf['resched'] # Daily cutoff ########################################################################## @@ -1299,6 +1303,12 @@ To study outside of the normal schedule, click the Custom Study button below.""" def nextIvl(self, card, ease): "Return the next interval for CARD, in seconds." + # preview mode? + if self._previewingCard(card): + if ease == 1: + return self._previewDelay + return 0 + # (re)learning? if card.queue in (0,1,3): return self._nextLrnIvl(card, ease) @@ -1312,10 +1322,7 @@ To study outside of the normal schedule, click the Custom Study button below.""" # review early = card.odid and (card.odue > self.today) if early: - if self._resched(card): - return self._earlyReviewIvl(card, ease)*86400 - else: - return 0 + return self._earlyReviewIvl(card, ease)*86400 else: return self._nextRevIvl(card, ease, fuzz=False)*86400 @@ -1330,16 +1337,11 @@ To study outside of the normal schedule, click the Custom Study button below.""" elif ease == 2: return self._delayForRepeatingGrade(conf, card.left) elif ease == 4: - # early removal - if not self._resched(card): - return 0 return self._graduatingIvl(card, conf, True, fuzz=False) * 86400 else: # ease == 3 left = card.left%1000 - 1 if left <= 0: # graduate - if not self._resched(card): - return 0 return self._graduatingIvl(card, conf, False, fuzz=False) * 86400 else: return self._delayForGrade(conf, left) diff --git a/tests/test_sched.py b/tests/test_sched.py index f30972ebd..d555524f5 100644 --- a/tests/test_sched.py +++ b/tests/test_sched.py @@ -627,112 +627,51 @@ def test_filt_keep_lrn_state(): c.load() assert c.type == c.queue == 1 -def test_filt_reschedoff(): - # add card +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() - # graduate should return it to new + # grab the first card c = d.sched.getCard() - ni = d.sched.nextIvl - assert ni(c, 1) == 60 - assert ni(c, 2) == (60+600)//2 - assert ni(c, 3) == 600 - assert ni(c, 4) == 0 - assert d.sched.nextIvlStr(c, 4) == "(end)" - d.sched.answerCard(c, 4) - assert c.queue == c.type == 0 - # undue reviews should also be unaffected - c.ivl = 100 - c.type = 2 - c.queue = 2 - c.due = d.sched.today + 25 - c.factor = STARTING_FACTOR - c.flush() - cardcopy = copy.copy(c) - d.sched.rebuildDyn(did) - d.reset() + assert d.sched.answerButtons(c) == 2 + assert d.sched.nextIvl(c, 1) == d.sched._previewDelay + 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) + + # the other card should appear again c = d.sched.getCard() - assert ni(c, 1) == 600 - assert ni(c, 2) == 0 - assert ni(c, 3) == 0 + assert c.id == orig.id + + # remove it d.sched.answerCard(c, 2) - assert c.ivl == 100 - assert c.due == d.sched.today + 25 - # check failure too - c = cardcopy - c.flush() - d.sched.rebuildDyn(did) - d.reset() - c = d.sched.getCard() - d.sched.answerCard(c, 1) - d.sched.emptyDyn(did) - c.load() - assert c.ivl == 100 - assert c.due == d.sched.today + 25 - # fail+grad early - c = cardcopy - c.flush() - d.sched.rebuildDyn(did) - d.reset() - c = d.sched.getCard() - d.sched.answerCard(c, 1) - d.sched.answerCard(c, 4) - d.sched.emptyDyn(did) - c.load() - assert c.ivl == 100 - assert c.due == d.sched.today + 25 - # due cards - pass - c = cardcopy - c.due = -25 - c.flush() - d.sched.rebuildDyn(did) - d.reset() - c = d.sched.getCard() - d.sched.answerCard(c, 3) - d.sched.emptyDyn(did) - c.load() - assert c.ivl == 100 - assert c.due == -25 - # fail - c = cardcopy - c.due = -25 - c.flush() - d.sched.rebuildDyn(did) - d.reset() - c = d.sched.getCard() - d.sched.answerCard(c, 1) - d.sched.emptyDyn(did) - c.load() - assert c.ivl == 100 - assert c.due == -25 - # fail with normal grad - c = cardcopy - c.due = -25 - c.flush() - d.sched.rebuildDyn(did) - d.reset() - c = d.sched.getCard() - d.sched.answerCard(c, 1) - d.sched.answerCard(c, 4) - c.load() - assert c.ivl == 100 - assert c.due == -25 - # lapsed card pulled into cram - # d.sched._cardConf(c)['lapse']['mult']=0.5 - # d.sched.answerCard(c, 1) - # d.sched.rebuildDyn(did) - # d.reset() - # c = d.sched.getCard() - # d.sched.answerCard(c, 2) - # print c.__dict__ + + # ensure it's in the same state as it started + assert c.queue == 0 + assert c.reps == 0 + assert c.type == 0 def test_ordcycle(): d = getEmptyCol() @@ -1067,56 +1006,3 @@ def test_failmult(): # so the card is reset to new d.sched.answerCard(c, 1) assert c.ivl == 1 - -# answering a new card with scheduling off should not change -# the original position -def test_preview_order(): - d = getEmptyCol() - f = d.newNote() - f['Front'] = "oneone" - d.addNote(f) - f = d.newNote() - f['Front'] = "twotwo" - d.addNote(f) - assert d.getCard(d.findCards("oneone")[0]).due == 1 - assert d.getCard(d.findCards("twotwo")[0]).due == 2 - - did = d.decks.newDyn("Cram") - cram = d.decks.get(did) - cram['resched'] = False - d.sched.rebuildDyn(did) - d.reset() - - c = d.sched.getCard() - assert "oneone" in c.q() - d.sched.answerCard(c, 3) - d.sched.answerCard(c, 3) - - assert c.due == 1 - -# answering a due review with scheduling off should not change scheduling -def test_reviews_reschedoff(): - d = getEmptyCol() - f = d.newNote() - f['Front'] = "one" - d.addNote(f) - - c = f.cards()[0] - c.ivl = 100 - c.queue = c.type = 2 - c.due = d.sched.today - c.factor = 2500 - c.flush() - - did = d.decks.newDyn("Cram") - cram = d.decks.get(did) - cram['resched'] = False - d.sched.rebuildDyn(did) - d.reset() - - c = d.sched.getCard() - d.sched.answerCard(c, 4) - - assert c.ivl == 100 - assert c.due == d.sched.today - assert c.factor == 2500