def _rescheduleLrnCard(self, card: Card, conf: QueueConfig, delay: int | None = None) -> Any: # normal delay for the current step? if delay is None: delay = self._delayForGrade(conf, card.left) card.due = int(time.time() + delay) # due today? if card.due < self.day_cutoff: # add some randomness, up to 5 minutes or 25% maxExtra = min(300, int(delay * 0.25)) fuzz = random.randrange(0, max(1, maxExtra)) card.due = min(self.day_cutoff - 1, card.due + fuzz) card.queue = QUEUE_TYPE_LRN if card.due < (int_time() + self.col.conf["collapseTime"]): self.lrnCount += 1 # 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 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 return delay
def test_collapse(): col = getEmptyCol() # add a note note = col.newNote() note["Front"] = "one" col.addNote(note) # and another, so we don't get the same twice in a row note = col.newNote() note["Front"] = "two" col.addNote(note) col.reset() # first note c = col.sched.getCard() col.sched.answerCard(c, 1) # second note c2 = col.sched.getCard() assert c2.nid != c.nid col.sched.answerCard(c2, 1) # first should become available again, despite it being due in the future c3 = col.sched.getCard() assert c3.due > int_time() col.sched.answerCard(c3, 4) # answer other c4 = col.sched.getCard() col.sched.answerCard(c4, 4) assert not col.sched.getCard()
def render_all_latex( self, progress_cb: Callable[[int], bool] | None = None ) -> tuple[int, str] | None: """Render any LaTeX that is missing. If a progress callback is provided and it returns false, the operation will be aborted. If an error is encountered, returns (note_id, error_message) """ last_progress = time.time() checked = 0 for (nid, mid, flds) in self.col.db.execute( "select id, mid, flds from notes where flds like '%[%'"): model = self.col.models.get(mid) _html, errors = render_latex_returning_errors(flds, model, self.col, expand_clozes=True) if errors: return (nid, "\n".join(errors)) checked += 1 elap = time.time() - last_progress if elap >= 0.3 and progress_cb is not None: last_progress = int_time() if not progress_cb(checked): return None return None
def deck_due_tree(self, top_deck_id: DeckId | None = None) -> DeckTreeNode | None: """Returns a tree of decks with counts. If top_deck_id provided, only the according subtree is returned.""" tree = self.col._backend.deck_tree(now=int_time()) if top_deck_id: return self.col.decks.find_deck_in_tree(tree, top_deck_id) return tree
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() col.sched.answerCard(c, 1) # the next card should be another review c2 = col.sched.getCard() assert c2.queue == QUEUE_TYPE_REV # if the failed card becomes due, it should show first c.due = int_time() - 1 c.flush() col.reset() c = col.sched.getCard() assert c.queue == QUEUE_TYPE_LRN
def set_deck(self, cids: list[CardId], did: DeckId) -> None: self.col.set_deck(card_ids=cids, deck_id=did) self.col.db.execute( f"update cards set did=?,usn=?,mod=? where id in {ids2str(cids)}", did, self.col.usn(), int_time(), )
def seconds_since_last_sync(self) -> int: if self.is_syncing(): return 0 if self._log: last = self._log[-1].time else: last = 0 return int_time() - last
def test_filt_keep_lrn_state(): col = getEmptyCol() note = col.newNote() note["Front"] = "one" col.addNote(note) # fail the card outside filtered deck c = col.sched.getCard() conf = col.sched._cardConf(c) conf["new"]["delays"] = [1, 10, 61] col.decks.save(conf) col.sched.answerCard(c, 1) assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN assert c.left % 1000 == 3 col.sched.answerCard(c, 3) assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN # create a dynamic deck and refresh it did = col.decks.new_filtered("Cram") col.sched.rebuild_filtered_deck(did) col.reset() # card should still be in learning state c.load() assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN assert c.left % 1000 == 2 # should be able to advance learning steps col.sched.answerCard(c, 3) # should be due at least an hour in the future assert c.due - int_time() > 60 * 60 # emptying the deck preserves learning state col.sched.empty_filtered_deck(did) c.load() assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN assert c.left % 1000 == 1 assert c.due - int_time() > 60 * 60
def answerCard(self, card: Card, ease: int) -> None: if (not 1 <= ease <= 4) or (not 0 <= card.queue <= 4): raise Exception("invalid ease or queue") self.col.save_card_review_undo_info(card) if self._burySiblingsOnAnswer: self._burySiblings(card) self._answerCard(card, ease) card.mod = int_time() card.usn = self.col.usn() card.flush()
def _answerCardPreview(self, card: Card, ease: int) -> None: if not 1 <= ease <= 2: raise Exception("invalid ease") if ease == BUTTON_ONE: # repeat after delay card.queue = QUEUE_TYPE_PREVIEW card.due = int_time() + self._previewDelay(card) self.lrnCount += 1 else: # BUTTON_TWO # restore original card state and remove from filtered deck self._restorePreviewCard(card) self._removeFromFiltered(card)
def updateData(self, n: ForeignNote, id: NoteId, sflds: list[str]) -> Optional[Updates]: self._ids.append(id) self.processFields(n, sflds) if self._tagsMapped: tags = self.col.tags.join(n.tags) return ( int_time(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr, tags, ) elif self.tagModified: tags = self.col.db.scalar("select tags from notes where id = ?", id) tagList = self.col.tags.split(tags) + self.tagModified.split() tags = self.col.tags.join(tagList) return (int_time(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr) else: return (int_time(), self.col.usn(), n.fieldsStr, id, n.fieldsStr)
def _leftToday( self, delays: list[int], left: int, now: int | None = None, ) -> int: "The number of steps that can be completed by the day cutoff." if not now: now = int_time() delays = delays[-left:] ok = 0 for idx, delay in enumerate(delays): now += int(delay * 60) if now > self.day_cutoff: break ok = idx return ok + 1
def _fillLrn(self) -> bool | list[Any]: if not self.lrnCount: return False if self._lrnQueue: return True cutoff = int_time() + self.col.conf["collapseTime"] self._lrnQueue = self.col.db.all( # type: ignore f""" select due, id from cards where did in %s and queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_PREVIEW}) and due < ? limit %d""" % (self._deck_limit(), self.reportLimit), cutoff, ) self._lrnQueue = [cast(tuple[int, CardId], 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 _did(self, did: DeckId) -> Any: "Given did in src col, return local id." # already converted? if did in self._decks: return self._decks[did] # get the name in src g = self.src.decks.get(did) name = g["name"] # if there's a prefix, replace the top level deck if self.deckPrefix: tmpname = "::".join(DeckManager.path(name)[1:]) name = self.deckPrefix if tmpname: name += f"::{tmpname}" # manually create any parents so we can pull in descriptions head = "" for parent in DeckManager.immediate_parent_path(name): if head: head += "::" head += parent idInSrc = self.src.decks.id(head) self._did(idInSrc) # if target is a filtered deck, we'll need a new deck name deck = self.dst.decks.by_name(name) if deck and deck["dyn"]: name = "%s %d" % (name, int_time()) # create in local newid = self.dst.decks.id(name) # pull conf over if "conf" in g and g["conf"] != 1: conf = self.src.decks.get_config(g["conf"]) self.dst.decks.save(conf) self.dst.decks.update_config(conf) g2 = self.dst.decks.get(newid) g2["conf"] = g["conf"] self.dst.decks.save(g2) # save desc deck = self.dst.decks.get(newid) deck["desc"] = g["desc"] self.dst.decks.save(deck) # add to deck map and return self._decks[did] = newid return newid
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
def build_answer(self, *, card: Card, states: NextStates, rating: CardAnswer.Rating.V) -> CardAnswer: "Build input for answer_card()." if rating == CardAnswer.AGAIN: new_state = states.again elif rating == CardAnswer.HARD: new_state = states.hard elif rating == CardAnswer.GOOD: new_state = states.good elif rating == CardAnswer.EASY: new_state = states.easy else: raise Exception("invalid rating") return CardAnswer( card_id=card.id, current_state=states.current, new_state=new_state, rating=rating, answered_at_millis=int_time(1000), milliseconds_taken=card.time_taken(capped=False), )
def newData( self, n: ForeignNote ) -> tuple[NoteId, str, NotetypeId, int, int, str, str, str, int, int, str]: id = self._nextID self._nextID = NoteId(self._nextID + 1) self._ids.append(id) self.processFields(n) # note id for card updates later for ord, c in list(n.cards.items()): self._cards.append((id, ord, c)) return ( id, guid64(), self.model["id"], int_time(), self.col.usn(), self.col.tags.join(n.tags), n.fieldsStr, "", 0, 0, "", )
else: return VideoDriver.Software @staticmethod def all_for_platform() -> list[VideoDriver]: all = [VideoDriver.OpenGL] if is_win: all.append(VideoDriver.ANGLE) all.append(VideoDriver.Software) return all metaConf = dict( ver=0, updates=True, created=int_time(), id=random.randrange(0, 2**63), lastMsg=-1, suppressUpdate=False, firstRun=True, defaultLang=None, ) profileConf: dict[str, Any] = dict( # profile mainWindowGeom=None, mainWindowState=None, numBackups=50, lastOptimize=int_time(), # editing searchHistory=[],
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 _importCards(self) -> None: if self.source_needs_upgrade: self.src.upgrade_to_v2_scheduler() # build map of (guid, ord) -> cid and used id cache self._cards: dict[tuple[str, int], CardId] = {} existing = {} for guid, ord, cid in self.dst.db.execute( "select f.guid, c.ord, c.id from cards c, notes f " "where c.nid = f.id" ): existing[cid] = True self._cards[(guid, ord)] = cid # loop through src cards = [] revlog = [] cnt = 0 usn = self.dst.usn() aheadBy = self.src.sched.today - self.dst.sched.today for card in self.src.db.execute( "select f.guid, f.mid, c.* from cards c, notes f " "where c.nid = f.id" ): guid = card[0] if guid in self._ignoredGuids: continue # does the card's note exist in dst col? if guid not in self._notes: continue # does the card already exist in the dst col? ord = card[5] if (guid, ord) in self._cards: # fixme: in future, could update if newer mod time continue # doesn't exist. strip off note info, and save src id for later card = list(card[2:]) scid = card[0] # ensure the card id is unique while card[0] in existing: card[0] += 999 existing[card[0]] = True # update cid, nid, etc card[1] = self._notes[guid][0] card[2] = self._did(card[2]) card[4] = int_time() card[5] = usn # review cards have a due date relative to collection if ( card[7] in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN) or card[6] == CARD_TYPE_REV ): card[8] -= aheadBy # odue needs updating too if card[14]: card[14] -= aheadBy # if odid true, convert card from filtered to normal if card[15]: # odid card[15] = 0 # odue card[8] = card[14] card[14] = 0 # queue if card[6] == CARD_TYPE_LRN: # type card[7] = QUEUE_TYPE_NEW else: card[7] = card[6] # type if card[6] == CARD_TYPE_LRN: card[6] = CARD_TYPE_NEW cards.append(card) # we need to import revlog, rewriting card ids and bumping usn for rev in self.src.db.execute("select * from revlog where cid = ?", scid): rev = list(rev) rev[1] = card[0] rev[2] = self.dst.usn() revlog.append(rev) cnt += 1 # apply self.dst.db.executemany( """ insert or ignore into cards values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", cards, ) self.dst.db.executemany( """ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog, )
def deck_due_tree(self, top_deck_id: int = 0) -> DeckTreeNode: """Returns a tree of decks with counts. If top_deck_id provided, counts are limited to that node.""" return self.col._backend.deck_tree(top_deck_id=top_deck_id, now=int_time())
def _log_and_notify(self, entry: LogEntry) -> None: entry_with_time = LogEntryWithTime(time=int_time(), entry=entry) self._log.append(entry_with_time) self.mw.taskman.run_on_main( lambda: gui_hooks.media_sync_did_progress(entry_with_time) )
def _updateLrnCutoff(self, force: bool) -> bool: nextCutoff = int_time() + self.col.conf["collapseTime"] if nextCutoff - self._lrnCutoff > 60 or force: self._lrnCutoff = nextCutoff return True return False