From 7f40d6d2a500086227f507b2b59475e2337cc598 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 24 Nov 2021 13:33:01 +1000 Subject: [PATCH] retire the v1 scheduler --- ftl/core/scheduling.ftl | 3 + pylib/anki/scheduler/v1.py | 687 +--------------------- pylib/tests/test_schedv1.py | 1074 ----------------------------------- qt/aqt/deckbrowser.py | 2 +- qt/aqt/reviewer.py | 3 + 5 files changed, 31 insertions(+), 1738 deletions(-) delete mode 100644 pylib/tests/test_schedv1.py diff --git a/ftl/core/scheduling.ftl b/ftl/core/scheduling.ftl index 0cd733ac7..14e5c3b13 100644 --- a/ftl/core/scheduling.ftl +++ b/ftl/core/scheduling.ftl @@ -98,6 +98,9 @@ scheduling-update-done = Scheduler updated successfully. scheduling-update-button = Update scheduling-update-later-button = Later scheduling-update-more-info-button = Learn More +scheduling-update-required = + Your collection needs to be upgraded to the V2 scheduler. + Please select { scheduling-update-more-info-button } before proceeding. ## Other scheduling strings diff --git a/pylib/anki/scheduler/v1.py b/pylib/anki/scheduler/v1.py index 426f3006b..28c89b53a 100644 --- a/pylib/anki/scheduler/v1.py +++ b/pylib/anki/scheduler/v1.py @@ -5,24 +5,14 @@ from __future__ import annotations -import random -import time -from heapq import * - import anki -from anki import hooks from anki.cards import Card from anki.consts import * from anki.decks import DeckId -from anki.utils import ids2str, int_time from .v2 import QueueConfig from .v2 import Scheduler as V2 -# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried -# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram -# positive revlog intervals are in days (rev), negative in seconds (lrn) - class Scheduler(V2): version = 1 @@ -35,689 +25,60 @@ class Scheduler(V2): self, col: anki.collection.Collection ) -> None: super().__init__(col) - self.queueLimit = 50 - self.reportLimit = 1000 - self.dynReportLimit = 99999 + self.queueLimit = 0 + self.reportLimit = 0 + self.dynReportLimit = 0 self.reps = 0 self.lrnCount = 0 self.revCount = 0 self.newCount = 0 self._haveQueues = False + def reset(self) -> None: + pass + + def getCard(self) -> Card | None: + raise Exception("v1 scheduler no longer supported") + def answerCard(self, card: Card, ease: int) -> None: - assert 1 <= ease <= 4 - self.col.save_card_review_undo_info(card) - if self._burySiblingsOnAnswer: - self._burySiblings(card) - card.reps += 1 - self.reps += 1 - # former is for logging new cards, latter also covers filt. decks - card.wasNew = card.type == CARD_TYPE_NEW # type: ignore - wasNewQ = card.queue == QUEUE_TYPE_NEW + raise Exception("v1 scheduler no longer supported") - new_delta = 0 - review_delta = 0 + def _is_finished(self) -> bool: + return False - if wasNewQ: - # came from the new queue, move to learning - card.queue = QUEUE_TYPE_LRN - # if it was a new card, it's now a learning card - if card.type == CARD_TYPE_NEW: - card.type = CARD_TYPE_LRN - # init reps to graduation - card.left = self._startingLeft(card) - # dynamic? - if card.odid and card.type == CARD_TYPE_REV: - if self._resched(card): - # reviews get their ivl boosted on first sight - card.ivl = self._dynIvlBoost(card) - card.odue = self.today + card.ivl - new_delta = +1 - if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): - self._answerLrnCard(card, ease) - elif card.queue == QUEUE_TYPE_REV: - self._answerRevCard(card, ease) - review_delta = +1 - else: - raise Exception(f"Invalid queue '{card}'") - - self.update_stats( - card.did, - new_delta=new_delta, - review_delta=review_delta, - milliseconds_delta=+card.time_taken(), - ) - - card.mod = int_time() - card.usn = self.col.usn() - card.flush() - - def counts(self, card: Card | None = None) -> tuple[int, int, int]: - counts = [self.newCount, self.lrnCount, self.revCount] - if card: - idx = self.countIdx(card) - if idx == QUEUE_TYPE_LRN: - counts[int(QUEUE_TYPE_LRN)] += card.left // 1000 - else: - counts[idx] += 1 - - new, lrn, rev = counts - return (new, lrn, rev) - - def countIdx(self, card: Card) -> int: - if card.queue == QUEUE_TYPE_DAY_LEARN_RELEARN: - return QUEUE_TYPE_LRN - return card.queue - - def answerButtons(self, card: Card) -> int: - if card.odue: - # normal review in dyn deck? - if card.odid and card.queue == QUEUE_TYPE_REV: - return 4 - conf = self._lrnConf(card) - if card.type in (CARD_TYPE_NEW, CARD_TYPE_LRN) or len(conf["delays"]) > 1: - return 3 - return 2 - elif card.queue == QUEUE_TYPE_REV: - return 4 - else: - return 3 - - # Getting the next card - ########################################################################## - - def _getCard(self) -> Card | None: - "Return the next due card id, or None." - # learning card due? - c = self._getLrnCard() - if c: - return c - # new first, or time for one? - if self._timeForNewCard(): - c = self._getNewCard() - if c: - return c - # card due for review? - c = self._getRevCard() - if c: - return c - # day learning card due? - c = self._getLrnDayCard() - if c: - return c - # new cards left? - c = self._getNewCard() - if c: - return c - # collapse or finish - return self._getLrnCard(collapse=True) - - # Learning queues - ########################################################################## - - def _resetLrnCount(self) -> None: - # sub-day - self.lrnCount = ( - self.col.db.scalar( - f""" -select sum(left/1000) from (select left from cards where -did in %s and queue = {QUEUE_TYPE_LRN} and due < ? limit %d)""" - % (self._deck_limit(), self.reportLimit), - self.day_cutoff, - ) - or 0 - ) - # day - self.lrnCount += self.col.db.scalar( - f""" -select count() from cards where did in %s and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} -and due <= ? limit %d""" - % (self._deck_limit(), self.reportLimit), - self.today, - ) - - def _resetLrn(self) -> None: - self._resetLrnCount() - self._lrnQueue: list[Any] = [] - self._lrnDayQueue: list[Any] = [] - self._lrnDids = self.col.decks.active()[:] - - # sub-day learning - def _fillLrn(self) -> bool | list[Any]: - if not self.lrnCount: - return False - if self._lrnQueue: - return True - self._lrnQueue = self.col.db.all( - f""" -select due, id from cards where -did in %s and queue = {QUEUE_TYPE_LRN} and due < ? -limit %d""" - % (self._deck_limit(), self.reportLimit), - self.day_cutoff, - ) - self._lrnQueue = [tuple(e) for e in self._lrnQueue] - # as it arrives sorted by did first, we need to sort it - self._lrnQueue.sort() - return self._lrnQueue - - def _getLrnCard(self, collapse: bool = False) -> Card | None: - if self._fillLrn(): - cutoff = time.time() - if collapse: - cutoff += self.col.conf["collapseTime"] - if self._lrnQueue[0][0] < cutoff: - id = heappop(self._lrnQueue)[1] - card = self.col.getCard(id) - self.lrnCount -= card.left // 1000 - return card - return None - - def _answerLrnCard(self, card: Card, ease: int) -> None: - # ease 1=no, 2=yes, 3=remove - conf = self._lrnConf(card) - if card.odid and not card.wasNew: # type: ignore - type = REVLOG_CRAM - elif card.type == CARD_TYPE_REV: - type = REVLOG_RELRN - else: - type = REVLOG_LRN - leaving = False - # lrnCount was decremented once when card was fetched - lastLeft = card.left - # immediate graduate? - if ease == BUTTON_THREE: - self._rescheduleAsRev(card, conf, True) - leaving = True - # graduation time? - elif ease == BUTTON_TWO and (card.left % 1000) - 1 <= 0: - self._rescheduleAsRev(card, conf, False) - leaving = True - else: - # one step towards graduation - if ease == BUTTON_TWO: - # decrement real left count and recalculate left today - left = (card.left % 1000) - 1 - card.left = self._leftToday(conf["delays"], left) * 1000 + left - # failed - else: - card.left = self._startingLeft(card) - resched = self._resched(card) - if "mult" in conf and resched: - # review that's lapsed - card.ivl = max(1, conf["minInt"], int(card.ivl * conf["mult"])) - else: - # new card; no ivl adjustment - pass - if resched and card.odid: - card.odue = self.today + 1 - delay = self._delayForGrade(conf, card.left) - if card.due < time.time(): - # not collapsed; add some randomness - delay *= int(random.uniform(1, 1.25)) - card.due = int(time.time() + delay) - # due today? - if card.due < self.day_cutoff: - self.lrnCount += card.left // 1000 - # if the queue is not empty and there's nothing else to do, make - # sure we don't put it at the head of the queue and end up showing - # it twice in a row - card.queue = QUEUE_TYPE_LRN - if self._lrnQueue and not self.revCount and not self.newCount: - smallestDue = self._lrnQueue[0][0] - card.due = max(card.due, smallestDue + 1) - heappush(self._lrnQueue, (card.due, card.id)) - else: - # the card is due in one or more days, so we need to use the - # day learn queue - ahead = ((card.due - self.day_cutoff) // 86400) + 1 - card.due = self.today + ahead - card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN - self._logLrn(card, ease, conf, leaving, type, lastLeft) - - def _lrnConf(self, card: Card) -> QueueConfig: - if card.type == CARD_TYPE_REV: - return self._lapseConf(card) - else: - return self._newConf(card) - - def _rescheduleAsRev(self, card: Card, conf: QueueConfig, early: bool) -> None: - lapse = card.type == CARD_TYPE_REV - if lapse: - if self._resched(card): - card.due = max(self.today + 1, card.odue) - else: - card.due = card.odue - card.odue = 0 - else: - self._rescheduleNew(card, conf, early) - card.queue = QUEUE_TYPE_REV - card.type = CARD_TYPE_REV - # if we were dynamic, graduating means moving back to the old deck - resched = self._resched(card) - if card.odid: - card.did = card.odid - card.odue = 0 - card.odid = DeckId(0) - # if rescheduling is off, it needs to be set back to a new card - if not resched and not lapse: - card.queue = QUEUE_TYPE_NEW - card.type = CARD_TYPE_NEW - card.due = self.col.nextID("pos") - - def _startingLeft(self, card: Card) -> int: - if card.type == CARD_TYPE_REV: - conf = self._lapseConf(card) - else: - conf = self._lrnConf(card) - tot = len(conf["delays"]) - tod = self._leftToday(conf["delays"], tot) - return tot + tod * 1000 + # stubs of v1-specific routines that add-ons may be overriding def _graduatingIvl( self, card: Card, conf: QueueConfig, early: bool, adj: bool = True ) -> int: - if card.type == CARD_TYPE_REV: - # lapsed card being relearnt - if card.odid: - if conf["resched"]: - return self._dynIvlBoost(card) - return card.ivl - if not early: - # graduate - ideal = conf["ints"][0] - else: - # early remove - ideal = conf["ints"][1] - if adj: - return self._adjRevIvl(card, ideal) - else: - return ideal - - def _rescheduleNew(self, card: Card, conf: QueueConfig, early: bool) -> None: - "Reschedule a new card that's graduated for the first time." - card.ivl = self._graduatingIvl(card, conf, early) - card.due = self.today + card.ivl - card.factor = conf["initialFactor"] - - def _logLrn( - self, - card: Card, - ease: int, - conf: QueueConfig, - leaving: bool, - type: int, - lastLeft: int, - ) -> None: - lastIvl = -(self._delayForGrade(conf, lastLeft)) - ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) - - def log() -> None: - self.col.db.execute( - "insert into revlog values (?,?,?,?,?,?,?,?,?)", - int(time.time() * 1000), - card.id, - self.col.usn(), - ease, - ivl, - lastIvl, - card.factor, - card.time_taken(), - type, - ) - - try: - log() - except: - # duplicate pk; retry in 10ms - time.sleep(0.01) - log() + return 0 def removeLrn(self, ids: list[int] | None = None) -> None: - "Remove cards from the learning queues." - if ids: - extra = f" and id in {ids2str(ids)}" - else: - # benchmarks indicate it's about 10x faster to search all decks - # with the index than scan the table - extra = f" and did in {ids2str(d.id for d in self.col.decks.all_names_and_ids())}" - # review cards in relearning - self.col.db.execute( - f""" -update cards set -due = odue, queue = {QUEUE_TYPE_REV}, mod = %d, usn = %d, odue = 0 -where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CARD_TYPE_REV} -%s -""" - % (int_time(), self.col.usn(), extra) - ) - # new cards in learning - self.forgetCards( - self.col.db.list( - f"select id from cards where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) %s" - % extra - ) - ) + pass def _lrnForDeck(self, did: DeckId) -> int: - cnt = ( - self.col.db.scalar( - f""" -select sum(left/1000) from -(select left from cards where did = ? and queue = {QUEUE_TYPE_LRN} and due < ? limit ?)""", - did, - int_time() + self.col.conf["collapseTime"], - self.reportLimit, - ) - or 0 - ) - return cnt + self.col.db.scalar( - f""" -select count() from -(select 1 from cards where did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} -and due <= ? limit ?)""", - did, - self.today, - self.reportLimit, - ) - - # Reviews - ########################################################################## + return 0 def _deckRevLimit(self, did: DeckId) -> int: - return self._deckNewLimit(did, self._deckRevLimitSingle) - - def _resetRev(self) -> None: - self._revQueue: list[Any] = [] - self._revDids = self.col.decks.active()[:] - - def _fillRev(self, recursing: bool = False) -> bool: - "True if a review card can be fetched." - if self._revQueue: - return True - if not self.revCount: - return False - while self._revDids: - did = self._revDids[0] - lim = min(self.queueLimit, self._deckRevLimit(did)) - if lim: - # fill the queue with the current did - self._revQueue = self.col.db.list( - f""" -select id from cards where -did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""", - did, - self.today, - lim, - ) - if self._revQueue: - # ordering - if self.col.decks.get(did)["dyn"]: - # dynamic decks need due order preserved - self._revQueue.reverse() - else: - # random order for regular reviews - r = random.Random() - r.seed(self.today) - r.shuffle(self._revQueue) - # is the current did empty? - if len(self._revQueue) < lim: - self._revDids.pop(0) - return True - # nothing left in the deck; move to next - self._revDids.pop(0) - - # if we didn't get a card but the count is non-zero, - # we need to check again for any cards that were - # removed from the queue but not buried - if recursing: - print("bug: fillRev()") - return False - self._reset_counts() - self._resetRev() - return self._fillRev(recursing=True) - - # Answering a review card - ########################################################################## - - def _answerRevCard(self, card: Card, ease: int) -> None: - delay: int = 0 - if ease == BUTTON_ONE: - delay = self._rescheduleLapse(card) - else: - self._rescheduleRev(card, ease) - self._logRev(card, ease, delay, REVLOG_REV) - - def _rescheduleLapse(self, card: Card) -> int: - conf = self._lapseConf(card) - card.lastIvl = card.ivl - if self._resched(card): - card.lapses += 1 - card.ivl = self._nextLapseIvl(card, conf) - card.factor = max(1300, card.factor - 200) - card.due = self.today + card.ivl - # if it's a filtered deck, update odue as well - if card.odid: - card.odue = card.due - # if suspended as a leech, nothing to do - delay: int = 0 - if self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED: - return delay - # if no relearning steps, nothing to do - if not conf["delays"]: - return delay - # record rev due date for later - if not card.odue: - card.odue = card.due - delay = self._delayForGrade(conf, 0) - card.due = int(delay + time.time()) - card.left = self._startingLeft(card) - # queue 1 - if card.due < self.day_cutoff: - self.lrnCount += card.left // 1000 - card.queue = QUEUE_TYPE_LRN - heappush(self._lrnQueue, (card.due, card.id)) - else: - # day learn queue - ahead = ((card.due - self.day_cutoff) // 86400) + 1 - card.due = self.today + ahead - card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN - return delay + return 0 def _nextLapseIvl(self, card: Card, conf: QueueConfig) -> int: - return max(conf["minInt"], int(card.ivl * conf["mult"])) + return 0 def _rescheduleRev(self, card: Card, ease: int) -> None: # type: ignore[override] - # 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 - if card.odid: - card.did = card.odid - card.odid = DeckId(0) - card.odue = 0 - - # Interval management - ########################################################################## + pass def _nextRevIvl(self, card: Card, ease: int) -> int: # type: ignore[override] - "Ideal next interval for CARD, given EASE." - delay = self._daysLate(card) - conf = self._revConf(card) - fct = card.factor / 1000 - ivl2 = self._constrainedIvl((card.ivl + delay // 4) * 1.2, conf, card.ivl) - ivl3 = self._constrainedIvl((card.ivl + delay // 2) * fct, conf, ivl2) - ivl4 = self._constrainedIvl( - (card.ivl + delay) * fct * conf["ease4"], conf, ivl3 - ) - if ease == BUTTON_TWO: - interval = ivl2 - elif ease == BUTTON_THREE: - interval = ivl3 - elif ease == BUTTON_FOUR: - interval = ivl4 - # interval capped? - return min(interval, conf["maxIvl"]) + return 0 def _constrainedIvl(self, ivl: float, conf: QueueConfig, prev: int) -> int: # type: ignore[override] - "Integer interval after interval factor and prev+1 constraints applied." - new = ivl * conf.get("ivlFct", 1) - return int(max(new, prev + 1)) - - def _updateRevIvl(self, card: Card, ease: int) -> None: - idealIvl = self._nextRevIvl(card, ease) - card.ivl = min( - max(self._adjRevIvl(card, idealIvl), card.ivl + 1), - self._revConf(card)["maxIvl"], - ) + return 0 def _adjRevIvl(self, card: Card, idealIvl: int) -> int: - if self._spreadRev: - idealIvl = self._fuzzedIvl(idealIvl) - return idealIvl - - # Filtered deck handling - ########################################################################## + return 0 def _dynIvlBoost(self, card: Card) -> int: - assert card.odid and card.type == CARD_TYPE_REV - assert card.factor - elapsed = card.ivl - (card.odue - self.today) - factor = ((card.factor / 1000) + 1.2) / 2 - ivl = int(max(card.ivl, elapsed * factor, 1)) - conf = self._revConf(card) - return min(conf["maxIvl"], ivl) - - # Leeches - ########################################################################## - - def _checkLeech(self, card: Card, conf: QueueConfig) -> bool: - "Leech handler. True if card was a leech." - lf = conf["leechFails"] - if not lf: - return False - # if over threshold or every half threshold reps after that - if card.lapses >= lf and (card.lapses - lf) % (max(lf // 2, 1)) == 0: - # add a leech tag - f = card.note() - f.add_tag("leech") - f.flush() - # handle - a = conf["leechAction"] - if a == LEECH_SUSPEND: - # 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 = 0 - card.odid = DeckId(0) - card.queue = QUEUE_TYPE_SUSPENDED - # notify UI - hooks.card_did_leech(card) - return True - else: - return False - - # Tools - ########################################################################## - - def _newConf(self, card: Card) -> QueueConfig: - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf["new"] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.config_dict_for_deck_id(card.odid) - delays = conf["delays"] or oconf["new"]["delays"] - return dict( - # original deck - ints=oconf["new"]["ints"], - initialFactor=oconf["new"]["initialFactor"], - bury=oconf["new"].get("bury", True), - # overrides - delays=delays, - order=NEW_CARDS_DUE, - perDay=self.reportLimit, - ) - - def _lapseConf(self, card: Card) -> QueueConfig: - conf = self._cardConf(card) - # normal deck - if not card.odid: - return conf["lapse"] - # dynamic deck; override some attributes, use original deck for others - oconf = self.col.decks.config_dict_for_deck_id(card.odid) - delays = conf["delays"] or oconf["lapse"]["delays"] - return dict( - # original deck - minInt=oconf["lapse"]["minInt"], - leechFails=oconf["lapse"]["leechFails"], - leechAction=oconf["lapse"]["leechAction"], - mult=oconf["lapse"]["mult"], - # overrides - delays=delays, - resched=conf["resched"], - ) + return 0 def _resched(self, card: Card) -> bool: - conf = self._cardConf(card) - if not conf["dyn"]: - return True - return conf["resched"] - - # Deck finished state - ########################################################################## - - def have_buried(self) -> bool: - sdids = self._deck_limit() - cnt = self.col.db.scalar( - f"select 1 from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s limit 1" - % sdids - ) - return bool(cnt) - - # Next time reports - ########################################################################## - - def nextIvl(self, card: Card, ease: int) -> float: - "Return the next interval for CARD, in seconds." - if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): - return self._nextLrnIvl(card, ease) - elif ease == BUTTON_ONE: - # lapsed - conf = self._lapseConf(card) - if conf["delays"]: - return conf["delays"][0] * 60 - return self._nextLapseIvl(card, conf) * 86400 - else: - # review - return self._nextRevIvl(card, ease) * 86400 - - # this isn't easily extracted from the learn code - def _nextLrnIvl(self, card: Card, ease: int) -> float: - if card.queue == 0: - card.left = self._startingLeft(card) - conf = self._lrnConf(card) - if ease == BUTTON_ONE: - # fail - return self._delayForGrade(conf, len(conf["delays"])) - elif ease == BUTTON_THREE: - # early removal - if not self._resched(card): - return 0 - return self._graduatingIvl(card, conf, True, adj=False) * 86400 - else: - left = card.left % 1000 - 1 - if left <= 0: - # graduate - if not self._resched(card): - return 0 - return self._graduatingIvl(card, conf, False, adj=False) * 86400 - else: - return self._delayForGrade(conf, left) + return False diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py deleted file mode 100644 index 690cc0fd2..000000000 --- a/pylib/tests/test_schedv1.py +++ /dev/null @@ -1,1074 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -import copy -import time - -from anki.collection import Collection -from anki.consts import * -from anki.lang import without_unicode_isolation -from anki.utils import int_time -from tests.shared import getEmptyCol as getEmptyColOrig - - -def getEmptyCol() -> Collection: - col = getEmptyColOrig() - # only safe in test environment - col.set_config("schedVer", 1) - col._load_scheduler() - return col - - -def test_clock(): - col = getEmptyCol() - if (col.sched.day_cutoff - int_time()) < 10 * 60: - raise Exception("Unit tests will fail around the day rollover.") - - -def checkRevIvl(col, c, targetIvl): - min, max = col.sched._fuzzIvlRange(targetIvl) - return min <= c.ivl <= max - - -def test_basics(): - col = getEmptyCol() - col.reset() - assert not col.sched.getCard() - - -def test_new(): - col = getEmptyCol() - col.reset() - assert col.sched.newCount == 0 - # add a note - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - col.reset() - assert col.sched.newCount == 1 - # fetch it - c = col.sched.getCard() - assert c - assert c.queue == QUEUE_TYPE_NEW - assert c.type == CARD_TYPE_NEW - # if we answer it, it should become a learn card - t = int_time() - col.sched.answerCard(c, 1) - assert c.queue == QUEUE_TYPE_LRN - assert c.type == CARD_TYPE_LRN - assert c.due >= t - - # disabled for now, as the learn fudging makes this randomly fail - # # the default order should ensure siblings are not seen together, and - # # should show all cards - # 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'] = u"2"; note['Back'] = u"2" - # col.addNote(note) - # note = col.newNote() - # note['Front'] = u"3"; note['Back'] = u"3" - # col.addNote(note) - # col.reset() - # qs = ("2", "3", "2", "3") - # for n in range(4): - # c = col.sched.getCard() - # assert qs[n] in c.question() - # col.sched.answerCard(c, 2) - - -def test_newLimits(): - col = getEmptyCol() - # add some notes - deck2 = col.decks.id("Default::foo") - for i in range(30): - note = col.newNote() - note["Front"] = str(i) - if i > 4: - note.note_type()["did"] = deck2 - col.addNote(note) - # give the child deck a different configuration - c2 = col.decks.add_config_returning_id("new conf") - col.decks.set_config_id_for_deck_dict(col.decks.get(deck2), c2) - col.reset() - # both confs have defaulted to a limit of 20 - assert col.sched.newCount == 20 - # first card we get comes from parent - c = col.sched.getCard() - assert c.did == 1 - # limit the parent to 10 cards, meaning we get 10 in total - conf1 = col.decks.config_dict_for_deck_id(1) - conf1["new"]["perDay"] = 10 - col.decks.save(conf1) - col.reset() - assert col.sched.newCount == 10 - # if we limit child to 4, we should get 9 - conf2 = col.decks.config_dict_for_deck_id(deck2) - conf2["new"]["perDay"] = 4 - col.decks.save(conf2) - col.reset() - assert col.sched.newCount == 9 - - -def test_newBoxes(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - col.reset() - c = col.sched.getCard() - conf = col.sched._cardConf(c) - conf["new"]["delays"] = [1, 2, 3, 4, 5] - col.decks.save(conf) - col.sched.answerCard(c, 2) - # should handle gracefully - conf["new"]["delays"] = [1] - col.decks.save(conf) - col.sched.answerCard(c, 2) - - -def test_learn(): - col = getEmptyCol() - # add a note - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - # set as a new card and rebuild queues - col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}") - col.reset() - # sched.getCard should return it, since it's due in the past - c = col.sched.getCard() - assert c - conf = col.sched._cardConf(c) - conf["new"]["delays"] = [0.5, 3, 10] - col.decks.save(conf) - # fail it - col.sched.answerCard(c, 1) - # it should have three reps left to graduation - assert c.left % 1000 == 3 - assert c.left // 1000 == 3 - # it should be due in 30 seconds - t = round(c.due - time.time()) - assert t >= 25 and t <= 40 - # pass it once - col.sched.answerCard(c, 2) - # it should be due in 3 minutes - assert round(c.due - time.time()) in (179, 180) - assert c.left % 1000 == 2 - assert c.left // 1000 == 2 - # check log is accurate - log = col.db.first("select * from revlog order by id desc") - assert log[3] == 2 - assert log[4] == -180 - assert log[5] == -30 - # pass again - col.sched.answerCard(c, 2) - # it should be due in 10 minutes - assert round(c.due - time.time()) in (599, 600) - assert c.left % 1000 == 1 - assert c.left // 1000 == 1 - # the next pass should graduate the card - assert c.queue == QUEUE_TYPE_LRN - assert c.type == CARD_TYPE_LRN - col.sched.answerCard(c, 2) - assert c.queue == QUEUE_TYPE_REV - assert c.type == CARD_TYPE_REV - # should be due tomorrow, with an interval of 1 - assert c.due == col.sched.today + 1 - assert c.ivl == 1 - # or normal removal - c.type = CARD_TYPE_NEW - c.queue = QUEUE_TYPE_LRN - col.sched.answerCard(c, 3) - assert c.type == CARD_TYPE_REV - assert c.queue == QUEUE_TYPE_REV - assert checkRevIvl(col, c, 4) - # revlog should have been updated each time - assert col.db.scalar("select count() from revlog where type = 0") == 5 - # now failed card handling - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_LRN - c.odue = 123 - col.sched.answerCard(c, 3) - assert c.due == 123 - assert c.type == CARD_TYPE_REV - assert c.queue == QUEUE_TYPE_REV - # we should be able to remove manually, too - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_LRN - c.odue = 321 - c.flush() - col.sched.removeLrn() - c.load() - assert c.queue == QUEUE_TYPE_REV - assert c.due == 321 - - -def test_learn_collapsed(): - col = getEmptyCol() - # add 2 notes - note = col.newNote() - note["Front"] = "1" - col.addNote(note) - note = col.newNote() - note["Front"] = "2" - col.addNote(note) - # set as a new card and rebuild queues - col.db.execute(f"update cards set queue={QUEUE_TYPE_NEW}, type={CARD_TYPE_NEW}") - col.reset() - # should get '1' first - c = col.sched.getCard() - assert c.question().endswith("1") - # pass it so it's due in 10 minutes - col.sched.answerCard(c, 2) - # get the other card - c = col.sched.getCard() - assert c.question().endswith("2") - # fail it so it's due in 1 minute - col.sched.answerCard(c, 1) - # we shouldn't get the same card again - c = col.sched.getCard() - assert not c.question().endswith("2") - - -def test_learn_day(): - col = getEmptyCol() - # add a note - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - col.sched.reset() - c = col.sched.getCard() - conf = col.sched._cardConf(c) - conf["new"]["delays"] = [1, 10, 1440, 2880] - col.decks.save(conf) - # pass it - col.sched.answerCard(c, 2) - # two reps to graduate, 1 more today - assert c.left % 1000 == 3 - assert c.left // 1000 == 1 - assert col.sched.counts() == (0, 1, 0) - c = col.sched.getCard() - ni = col.sched.nextIvl - assert ni(c, 2) == 86400 - # answering it will place it in queue 3 - col.sched.answerCard(c, 2) - assert c.due == col.sched.today + 1 - assert c.queue == CARD_TYPE_RELEARNING - assert not col.sched.getCard() - # for testing, move it back a day - c.due -= 1 - c.flush() - col.reset() - assert col.sched.counts() == (0, 1, 0) - c = col.sched.getCard() - # nextIvl should work - assert ni(c, 2) == 86400 * 2 - # if we fail it, it should be back in the correct queue - col.sched.answerCard(c, 1) - assert c.queue == QUEUE_TYPE_LRN - col.undo_legacy() - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 2) - # simulate the passing of another two days - c.due -= 2 - c.flush() - col.reset() - # the last pass should graduate it into a review card - assert ni(c, 2) == 86400 - col.sched.answerCard(c, 2) - assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV - # if the lapse step is tomorrow, failing it should handle the counts - # correctly - c.due = 0 - c.flush() - col.reset() - assert col.sched.counts() == (0, 0, 1) - conf = col.sched._cardConf(c) - conf["lapse"]["delays"] = [1440] - col.decks.save(conf) - c = col.sched.getCard() - col.sched.answerCard(c, 1) - assert c.queue == CARD_TYPE_RELEARNING - assert col.sched.counts() == (0, 0, 0) - - -def test_reviews(): - col = getEmptyCol() - # add a note - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - # set the card up as a review card, due 8 days ago - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_REV - c.due = col.sched.today - 8 - c.factor = STARTING_FACTOR - c.reps = 3 - c.lapses = 1 - c.ivl = 100 - c.start_timer() - c.flush() - # save it for later use as well - cardcopy = copy.copy(c) - # failing it should put it in the learn queue with the default options - ################################################## - # different delay to new - col.reset() - conf = col.sched._cardConf(c) - conf["lapse"]["delays"] = [2, 20] - col.decks.save(conf) - col.sched.answerCard(c, 1) - assert c.queue == QUEUE_TYPE_LRN - # it should be due tomorrow, with an interval of 1 - assert c.odue == col.sched.today + 1 - assert c.ivl == 1 - # but because it's in the learn queue, its current due time should be in - # the future - assert c.due >= time.time() - assert (c.due - time.time()) > 118 - # factor should have been decremented - assert c.factor == 2300 - # check counters - assert c.lapses == 2 - assert c.reps == 4 - # check ests. - ni = col.sched.nextIvl - assert ni(c, 1) == 120 - assert ni(c, 2) == 20 * 60 - # try again with an ease of 2 instead - ################################################## - c = copy.copy(cardcopy) - c.flush() - col.sched.answerCard(c, 2) - assert c.queue == QUEUE_TYPE_REV - # the new interval should be (100 + 8/4) * 1.2 = 122 - assert checkRevIvl(col, c, 122) - assert c.due == col.sched.today + c.ivl - # factor should have been decremented - assert c.factor == 2350 - # check counters - assert c.lapses == 1 - assert c.reps == 4 - # ease 3 - ################################################## - c = copy.copy(cardcopy) - c.flush() - col.sched.answerCard(c, 3) - # the new interval should be (100 + 8/2) * 2.5 = 260 - assert checkRevIvl(col, c, 260) - assert c.due == col.sched.today + c.ivl - # factor should have been left alone - assert c.factor == STARTING_FACTOR - # ease 4 - ################################################## - c = copy.copy(cardcopy) - c.flush() - col.sched.answerCard(c, 4) - # the new interval should be (100 + 8) * 2.5 * 1.3 = 351 - assert checkRevIvl(col, c, 351) - assert c.due == col.sched.today + c.ivl - # factor should have been increased - assert c.factor == 2650 - - -def test_button_spacing(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - # 1 day ivl review card due now - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_REV - c.due = col.sched.today - c.reps = 1 - c.ivl = 1 - c.start_timer() - c.flush() - col.reset() - ni = col.sched.nextIvlStr - wo = without_unicode_isolation - assert wo(ni(c, 2)) == "2d" - assert wo(ni(c, 3)) == "3d" - assert wo(ni(c, 4)) == "4d" - - -def test_overdue_lapse(): - # disabled in commit 3069729776990980f34c25be66410e947e9d51a2 - return - col = getEmptyCol() # pylint: disable=unreachable - # add a note - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - # simulate a review that was lapsed and is now due for its normal review - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_LRN - c.due = -1 - c.odue = -1 - c.factor = STARTING_FACTOR - c.left = 2002 - c.ivl = 0 - c.flush() - # checkpoint - col.save() - col.sched.reset() - assert col.sched.counts() == (0, 2, 0) - c = col.sched.getCard() - col.sched.answerCard(c, 3) - # it should be due tomorrow - assert c.due == col.sched.today + 1 - # revert to before - col.rollback() - # with the default settings, the overdue card should be removed from the - # learning queue - col.sched.reset() - assert col.sched.counts() == (0, 0, 1) - - -def test_nextIvl(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - col.reset() - conf = col.decks.config_dict_for_deck_id(1) - conf["new"]["delays"] = [0.5, 3, 10] - conf["lapse"]["delays"] = [1, 5, 9] - col.decks.save(conf) - c = col.sched.getCard() - # new cards - ################################################## - ni = col.sched.nextIvl - assert ni(c, 1) == 30 - assert ni(c, 2) == 180 - assert ni(c, 3) == 4 * 86400 - col.sched.answerCard(c, 1) - # cards in learning - ################################################## - assert ni(c, 1) == 30 - assert ni(c, 2) == 180 - assert ni(c, 3) == 4 * 86400 - col.sched.answerCard(c, 2) - assert ni(c, 1) == 30 - assert ni(c, 2) == 600 - assert ni(c, 3) == 4 * 86400 - col.sched.answerCard(c, 2) - # normal graduation is tomorrow - assert ni(c, 2) == 1 * 86400 - assert ni(c, 3) == 4 * 86400 - # lapsed cards - ################################################## - c.type = CARD_TYPE_REV - c.ivl = 100 - c.factor = STARTING_FACTOR - assert ni(c, 1) == 60 - assert ni(c, 2) == 100 * 86400 - assert ni(c, 3) == 100 * 86400 - # review cards - ################################################## - c.queue = QUEUE_TYPE_REV - c.ivl = 100 - c.factor = STARTING_FACTOR - # failing it should put it at 60s - assert ni(c, 1) == 60 - # or 1 day if relearn is false - conf["lapse"]["delays"] = [] - col.decks.save(conf) - 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 without_unicode_isolation(col.sched.nextIvlStr(c, 4)) == "10.8mo" - - -def test_misc(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - c = note.cards()[0] - # burying - col.sched.bury_notes([note.id]) - col.reset() - assert not col.sched.getCard() - col.sched.unbury_deck(deck_id=col.decks.get_current_id()) - col.reset() - assert col.sched.getCard() - - -def test_suspend(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - c = note.cards()[0] - # suspending - col.reset() - assert col.sched.getCard() - col.sched.suspend_cards([c.id]) - col.reset() - assert not col.sched.getCard() - # unsuspending - col.sched.unsuspend_cards([c.id]) - col.reset() - assert col.sched.getCard() - # should cope with rev cards being relearnt - c.due = 0 - c.ivl = 100 - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_REV - c.flush() - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 1) - assert c.due >= time.time() - assert c.queue == QUEUE_TYPE_LRN - assert c.type == CARD_TYPE_REV - col.sched.suspend_cards([c.id]) - col.sched.unsuspend_cards([c.id]) - c.load() - assert c.queue == QUEUE_TYPE_REV - assert c.type == CARD_TYPE_REV - assert c.due == 1 - # should cope with cards in cram decks - c.due = 1 - c.flush() - did = col.decks.new_filtered("tmp") - col.sched.rebuild_filtered_deck(did) - c.load() - assert c.due != 1 - assert c.did != 1 - col.sched.suspend_cards([c.id]) - c.load() - assert c.due == 1 - assert c.did == 1 - - -def test_cram(): - col = getEmptyCol() - opt = col.models.by_name("Basic (and reversed card)") - col.models.set_current(opt) - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - c = note.cards()[0] - c.ivl = 100 - c.queue = CARD_TYPE_REV - c.type = QUEUE_TYPE_REV - # due in 25 days, so it's been waiting 75 days - c.due = col.sched.today + 25 - c.mod = 1 - c.factor = STARTING_FACTOR - c.start_timer() - c.flush() - col.reset() - assert col.sched.counts() == (0, 0, 0) - cardcopy = copy.copy(c) - # create a dynamic deck and refresh it - did = col.decks.new_filtered("Cram") - col.sched.rebuild_filtered_deck(did) - col.reset() - # should appear as new in the deck list - assert sorted(col.sched.deck_due_tree().children)[0].new_count == 1 - # and should appear in the counts - assert col.sched.counts() == (1, 0, 0) - # grab it and check estimates - c = col.sched.getCard() - assert col.sched.answerButtons(c) == 2 - assert col.sched.nextIvl(c, 1) == 600 - assert col.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24 - cram = col.decks.get(did) - cram["delays"] = [1, 10] - col.decks.save(cram) - assert col.sched.answerButtons(c) == 3 - assert col.sched.nextIvl(c, 1) == 60 - assert col.sched.nextIvl(c, 2) == 600 - assert col.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24 - col.sched.answerCard(c, 2) - # elapsed time was 75 days - # factor = 2.5+1.2/2 = 1.85 - # int(75*1.85) = 138 - assert c.ivl == 138 - assert c.odue == 138 - assert c.queue == QUEUE_TYPE_LRN - # should be logged as a cram rep - assert col.db.scalar("select type from revlog order by id desc limit 1") == 3 - # check ivls again - assert col.sched.nextIvl(c, 1) == 60 - assert col.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24 - assert col.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24 - # when it graduates, due is updated - c = col.sched.getCard() - col.sched.answerCard(c, 2) - assert c.ivl == 138 - assert c.due == 138 - assert c.queue == QUEUE_TYPE_REV - # and it will have moved back to the previous deck - assert c.did == 1 - # cram the deck again - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - # check ivls again - passing should be idempotent - assert col.sched.nextIvl(c, 1) == 60 - assert col.sched.nextIvl(c, 2) == 600 - assert col.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24 - col.sched.answerCard(c, 2) - assert c.ivl == 138 - assert c.odue == 138 - # fail - col.sched.answerCard(c, 1) - assert col.sched.nextIvl(c, 1) == 60 - assert col.sched.nextIvl(c, 2) == 600 - assert col.sched.nextIvl(c, 3) == 86400 - # delete the deck, returning the card mid-study - col.decks.remove([col.decks.selected()]) - assert len(col.sched.deck_due_tree().children) == 1 - c.load() - assert c.ivl == 1 - assert c.due == col.sched.today + 1 - # make it due - col.reset() - assert col.sched.counts() == (0, 0, 0) - c.due = -5 - c.ivl = 100 - c.flush() - col.reset() - assert col.sched.counts() == (0, 0, 1) - # cram again - did = col.decks.new_filtered("Cram") - col.sched.rebuild_filtered_deck(did) - col.reset() - assert col.sched.counts() == (0, 0, 1) - c.load() - assert col.sched.answerButtons(c) == 4 - # add a sibling so we can test minSpace, etc - note["Back"] = "foo" - note.flush() - c2 = note.cards()[1] - c2.ord = 1 - c2.due = 325 - c2.flush() - # should be able to answer it - c = col.sched.getCard() - col.sched.answerCard(c, 4) - # it should have been moved back to the original deck - assert c.did == 1 - - -def test_cram_rem(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - oldDue = note.cards()[0].due - did = col.decks.new_filtered("Cram") - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 2) - # answering the card will put it in the learning queue - assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN - assert c.due != oldDue - # if we terminate cramming prematurely it should be set back to new - col.sched.empty_filtered_deck(did) - c.load() - assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW - assert c.due == oldDue - - -def test_cram_resched(): - # add card - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - # cram deck - did = col.decks.new_filtered("Cram") - cram = col.decks.get(did) - cram["resched"] = False - col.decks.save(cram) - col.sched.rebuild_filtered_deck(did) - col.reset() - # graduate should return it to new - c = col.sched.getCard() - ni = col.sched.nextIvl - assert ni(c, 1) == 60 - assert ni(c, 2) == 600 - assert ni(c, 3) == 0 - assert col.sched.nextIvlStr(c, 3) == "(end)" - col.sched.answerCard(c, 3) - assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW - # undue reviews should also be unaffected - c.ivl = 100 - c.queue = CARD_TYPE_REV - c.type = QUEUE_TYPE_REV - c.due = col.sched.today + 25 - c.factor = STARTING_FACTOR - c.flush() - cardcopy = copy.copy(c) - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - assert ni(c, 1) == 600 - assert ni(c, 2) == 0 - assert ni(c, 3) == 0 - col.sched.answerCard(c, 2) - assert c.ivl == 100 - assert c.due == col.sched.today + 25 - # check failure too - c = cardcopy - c.flush() - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 1) - col.sched.empty_filtered_deck(did) - c.load() - assert c.ivl == 100 - assert c.due == col.sched.today + 25 - # fail+grad early - c = cardcopy - c.flush() - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 1) - col.sched.answerCard(c, 3) - col.sched.empty_filtered_deck(did) - c.load() - assert c.ivl == 100 - assert c.due == col.sched.today + 25 - # due cards - pass - c = cardcopy - c.due = -25 - c.flush() - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 3) - col.sched.empty_filtered_deck(did) - c.load() - assert c.ivl == 100 - assert c.due == -25 - # fail - c = cardcopy - c.due = -25 - c.flush() - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 1) - col.sched.empty_filtered_deck(did) - c.load() - assert c.ivl == 100 - assert c.due == -25 - # fail with normal grad - c = cardcopy - c.due = -25 - c.flush() - col.sched.rebuild_filtered_deck(did) - col.reset() - c = col.sched.getCard() - col.sched.answerCard(c, 1) - col.sched.answerCard(c, 3) - c.load() - assert c.ivl == 100 - assert c.due == -25 - # lapsed card pulled into cram - # col.sched._cardConf(c)['lapse']['mult']=0.5 - # col.sched.answerCard(c, 1) - # col.sched.rebuild_filtered_deck(did) - # col.reset() - # c = col.sched.getCard() - # col.sched.answerCard(c, 2) - # print c.__dict__ - - -def test_ordcycle(): - col = getEmptyCol() - # add two more templates and set second active - m = col.models.current() - mm = col.models - t = mm.new_template("Reverse") - t["qfmt"] = "{{Back}}" - t["afmt"] = "{{Front}}" - mm.add_template(m, t) - t = mm.new_template("f2") - t["qfmt"] = "{{Front}}2" - t["afmt"] = "{{Back}}" - mm.add_template(m, t) - mm.save(m) - # create a new note; it should have 3 cards - note = col.newNote() - note["Front"] = "1" - note["Back"] = "1" - col.addNote(note) - assert col.card_count() == 3 - col.reset() - # ordinals should arrive in order - assert col.sched.getCard().ord == 0 - assert col.sched.getCard().ord == 1 - assert col.sched.getCard().ord == 2 - - -def test_counts_idx(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - col.reset() - assert col.sched.counts() == (1, 0, 0) - c = col.sched.getCard() - # counter's been decremented but idx indicates 1 - assert col.sched.counts() == (0, 0, 0) - assert col.sched.countIdx(c) == 0 - # answer to move to learn queue - col.sched.answerCard(c, 1) - assert col.sched.counts() == (0, 2, 0) - # fetching again will decrement the count - c = col.sched.getCard() - assert col.sched.counts() == (0, 0, 0) - assert col.sched.countIdx(c) == 1 - # answering should add it back again - col.sched.answerCard(c, 1) - assert col.sched.counts() == (0, 2, 0) - - -def test_repCounts(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - col.reset() - # lrnReps should be accurate on pass/fail - assert col.sched.counts() == (1, 0, 0) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 2, 0) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 2, 0) - col.sched.answerCard(col.sched.getCard(), 2) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 2, 0) - col.sched.answerCard(col.sched.getCard(), 2) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 2) - assert col.sched.counts() == (0, 0, 0) - note = col.newNote() - note["Front"] = "two" - col.addNote(note) - col.reset() - # initial pass should be correct too - col.sched.answerCard(col.sched.getCard(), 2) - assert col.sched.counts() == (0, 1, 0) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 2, 0) - col.sched.answerCard(col.sched.getCard(), 3) - assert col.sched.counts() == (0, 0, 0) - # immediate graduate should work - note = col.newNote() - note["Front"] = "three" - col.addNote(note) - col.reset() - col.sched.answerCard(col.sched.getCard(), 3) - assert col.sched.counts() == (0, 0, 0) - # and failing a review should too - note = col.newNote() - note["Front"] = "three" - col.addNote(note) - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_REV - c.due = col.sched.today - c.flush() - col.reset() - assert col.sched.counts() == (0, 0, 1) - col.sched.answerCard(col.sched.getCard(), 1) - assert col.sched.counts() == (0, 1, 0) - - -def test_timing(): - col = getEmptyCol() - # add a few review cards, due today - for i in range(5): - note = col.newNote() - note["Front"] = f"num{str(i)}" - col.addNote(note) - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_REV - c.due = 0 - c.flush() - # fail the first one - col.reset() - c = col.sched.getCard() - # set a a fail delay of 4 seconds - conf = col.sched._cardConf(c) - conf["lapse"]["delays"][0] = 1 / 15.0 - col.decks.save(conf) - col.sched.answerCard(c, 1) - # the next card should be another review - c = col.sched.getCard() - assert c.queue == QUEUE_TYPE_REV - # but if we wait for a few seconds, the failed card should come back - orig_time = time.time - - def adjusted_time(): - return orig_time() + 5 - - time.time = adjusted_time - c = col.sched.getCard() - assert c.queue == QUEUE_TYPE_LRN - time.time = orig_time - - -def test_collapse(): - col = getEmptyCol() - # add a note - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - col.reset() - # test collapsing - c = col.sched.getCard() - col.sched.answerCard(c, 1) - c = col.sched.getCard() - col.sched.answerCard(c, 3) - assert not col.sched.getCard() - - -def test_deckDue(): - col = getEmptyCol() - # add a note with default deck - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - # and one that's a child - note = col.newNote() - note["Front"] = "two" - default1 = note.note_type()["did"] = col.decks.id("Default::1") - col.addNote(note) - # make it a review card - c = note.cards()[0] - c.queue = QUEUE_TYPE_REV - c.due = 0 - c.flush() - # add one more with a new deck - note = col.newNote() - note["Front"] = "two" - note.note_type()["did"] = col.decks.id("foo::bar") - col.addNote(note) - # and one that's a sibling - note = col.newNote() - note["Front"] = "three" - note.note_type()["did"] = col.decks.id("foo::baz") - col.addNote(note) - col.reset() - assert len(col.decks.all_names_and_ids()) == 5 - tree = col.sched.deck_due_tree().children - assert tree[0].name == "Default" - # sum of child and parent - assert tree[0].deck_id == 1 - assert tree[0].review_count == 1 - assert tree[0].new_count == 1 - # child count is just review - child = tree[0].children[0] - assert child.name == "1" - assert child.deck_id == default1 - assert child.review_count == 1 - assert child.new_count == 0 - # code should not fail if a card has an invalid deck - c.did = 12345 - c.flush() - col.sched.deck_due_tree() - - -def test_deckFlow(): - col = getEmptyCol() - # add a note with default deck - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - # and one that's a child - note = col.newNote() - note["Front"] = "two" - note.note_type()["did"] = col.decks.id("Default::2") - col.addNote(note) - # and another that's higher up - note = col.newNote() - note["Front"] = "three" - default1 = note.note_type()["did"] = col.decks.id("Default::1") - col.addNote(note) - # should get top level one first, then ::1, then ::2 - col.reset() - assert col.sched.counts() == (3, 0, 0) - for i in "one", "three", "two": - c = col.sched.getCard() - assert c.note()["Front"] == i - col.sched.answerCard(c, 2) - - -def test_norelearn(): - col = getEmptyCol() - # add a note - note = col.newNote() - note["Front"] = "one" - col.addNote(note) - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_REV - c.due = 0 - c.factor = STARTING_FACTOR - c.reps = 3 - c.lapses = 1 - c.ivl = 100 - c.start_timer() - c.flush() - col.reset() - col.sched.answerCard(c, 1) - col.sched._cardConf(c)["lapse"]["delays"] = [] - col.sched.answerCard(c, 1) - - -def test_failmult(): - col = getEmptyCol() - note = col.newNote() - note["Front"] = "one" - note["Back"] = "two" - col.addNote(note) - c = note.cards()[0] - c.type = CARD_TYPE_REV - c.queue = QUEUE_TYPE_REV - c.ivl = 100 - c.due = col.sched.today - c.ivl - c.factor = STARTING_FACTOR - c.reps = 3 - c.lapses = 1 - c.start_timer() - c.flush() - conf = col.sched._cardConf(c) - conf["lapse"]["mult"] = 0.5 - col.decks.save(conf) - c = col.sched.getCard() - col.sched.answerCard(c, 1) - assert c.ivl == 50 - col.sched.answerCard(c, 1) - assert c.ivl == 25 diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index b0962a3e0..cfc4a4d33 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -356,7 +356,7 @@ class DeckBrowser:
- {tr.scheduling_update_soon()} + {tr.scheduling_update_required()}