mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
split out common scheduler code into base.py, use scheduler/ dir
Also move the legacy aliases into a separate file
This commit is contained in:
parent
a0c47243b6
commit
ad973bb701
10 changed files with 592 additions and 818 deletions
|
@ -31,7 +31,7 @@ from anki.media import MediaManager, media_paths_from_col_path
|
||||||
from anki.models import ModelManager, NoteType
|
from anki.models import ModelManager, NoteType
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.sched import Scheduler as V1Scheduler
|
from anki.sched import Scheduler as V1Scheduler
|
||||||
from anki.scheduler import Scheduler as V2TestScheduler
|
from anki.scheduler.v3 import Scheduler as V3TestScheduler
|
||||||
from anki.schedv2 import Scheduler as V2Scheduler
|
from anki.schedv2 import Scheduler as V2Scheduler
|
||||||
from anki.sync import SyncAuth, SyncOutput, SyncStatus
|
from anki.sync import SyncAuth, SyncOutput, SyncStatus
|
||||||
from anki.tags import TagManager
|
from anki.tags import TagManager
|
||||||
|
@ -151,7 +151,7 @@ class Collection:
|
||||||
self.sched = V1Scheduler(self)
|
self.sched = V1Scheduler(self)
|
||||||
elif ver == 2:
|
elif ver == 2:
|
||||||
if self.is_2021_test_scheduler_enabled():
|
if self.is_2021_test_scheduler_enabled():
|
||||||
self.sched = V2TestScheduler(self) # type: ignore
|
self.sched = V3TestScheduler(self) # type: ignore
|
||||||
else:
|
else:
|
||||||
self.sched = V2Scheduler(self)
|
self.sched = V2Scheduler(self)
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ from anki.utils import ids2str, intTime
|
||||||
|
|
||||||
|
|
||||||
class Scheduler(V2):
|
class Scheduler(V2):
|
||||||
|
version = 1
|
||||||
name = "std"
|
name = "std"
|
||||||
haveCustomStudy = True
|
haveCustomStudy = True
|
||||||
_spreadRev = True
|
_spreadRev = True
|
||||||
|
@ -38,7 +39,6 @@ class Scheduler(V2):
|
||||||
self.lrnCount = 0
|
self.lrnCount = 0
|
||||||
self.revCount = 0
|
self.revCount = 0
|
||||||
self.newCount = 0
|
self.newCount = 0
|
||||||
self.today: Optional[int] = None
|
|
||||||
self._haveQueues = False
|
self._haveQueues = False
|
||||||
self._updateCutoff()
|
self._updateCutoff()
|
||||||
|
|
||||||
|
|
|
@ -1,531 +0,0 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
"""
|
|
||||||
This file contains experimental scheduler changes, and is not currently
|
|
||||||
used by Anki.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from heapq import *
|
|
||||||
from typing import Any, List, Optional, Sequence, Tuple, Union
|
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
|
||||||
import anki._backend.backend_pb2 as _pb
|
|
||||||
from anki import hooks
|
|
||||||
from anki.cards import Card
|
|
||||||
from anki.consts import *
|
|
||||||
from anki.decks import DeckConfig, DeckTreeNode, QueueConfig
|
|
||||||
from anki.notes import Note
|
|
||||||
from anki.types import assert_exhaustive
|
|
||||||
from anki.utils import from_json_bytes, ids2str, intTime
|
|
||||||
|
|
||||||
QueuedCards = _pb.GetQueuedCardsOut.QueuedCards
|
|
||||||
CongratsInfo = _pb.CongratsInfoOut
|
|
||||||
SchedTimingToday = _pb.SchedTimingTodayOut
|
|
||||||
UnburyCurrentDeck = _pb.UnburyCardsInCurrentDeckIn
|
|
||||||
BuryOrSuspend = _pb.BuryOrSuspendCardsIn
|
|
||||||
|
|
||||||
# fixme: reviewer.cardQueue/editCurrent/undo handling/retaining current card
|
|
||||||
|
|
||||||
|
|
||||||
class Scheduler:
|
|
||||||
is_2021 = True
|
|
||||||
|
|
||||||
def __init__(self, col: anki.collection.Collection) -> None:
|
|
||||||
self.col = col.weakref()
|
|
||||||
# don't rely on this, it will likely be removed out in the future
|
|
||||||
self.reps = 0
|
|
||||||
|
|
||||||
# Timing
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def timing_today(self) -> SchedTimingToday:
|
|
||||||
return self.col._backend.sched_timing_today()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def today(self) -> int:
|
|
||||||
return self.timing_today().days_elapsed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dayCutoff(self) -> int:
|
|
||||||
return self.timing_today().next_day_at
|
|
||||||
|
|
||||||
# Fetching the next card
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
# backend automatically resets queues as operations are performed
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_queued_cards(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
fetch_limit: int = 1,
|
|
||||||
intraday_learning_only: bool = False,
|
|
||||||
) -> Union[QueuedCards, CongratsInfo]:
|
|
||||||
info = self.col._backend.get_queued_cards(
|
|
||||||
fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only
|
|
||||||
)
|
|
||||||
kind = info.WhichOneof("value")
|
|
||||||
if kind == "queued_cards":
|
|
||||||
return info.queued_cards
|
|
||||||
elif kind == "congrats_info":
|
|
||||||
return info.congrats_info
|
|
||||||
else:
|
|
||||||
assert_exhaustive(kind)
|
|
||||||
assert False
|
|
||||||
|
|
||||||
def getCard(self) -> Optional[Card]:
|
|
||||||
"""Fetch the next card from the queue. None if finished."""
|
|
||||||
response = self.get_queued_cards()
|
|
||||||
if isinstance(response, QueuedCards):
|
|
||||||
backend_card = response.cards[0].card
|
|
||||||
card = Card(self.col)
|
|
||||||
card._load_from_backend_card(backend_card)
|
|
||||||
card.startTimer()
|
|
||||||
return card
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _is_finished(self) -> bool:
|
|
||||||
"Don't use this, it is a stop-gap until this code is refactored."
|
|
||||||
info = self.get_queued_cards()
|
|
||||||
return isinstance(info, CongratsInfo)
|
|
||||||
|
|
||||||
def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
|
|
||||||
info = self.get_queued_cards()
|
|
||||||
if isinstance(info, CongratsInfo):
|
|
||||||
counts = [0, 0, 0]
|
|
||||||
else:
|
|
||||||
counts = [info.new_count, info.learning_count, info.review_count]
|
|
||||||
|
|
||||||
return tuple(counts) # type: ignore
|
|
||||||
|
|
||||||
@property
|
|
||||||
def newCount(self) -> int:
|
|
||||||
return self.counts()[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lrnCount(self) -> int:
|
|
||||||
return self.counts()[1]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def reviewCount(self) -> int:
|
|
||||||
return self.counts()[2]
|
|
||||||
|
|
||||||
# Answering a card
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def answerCard(self, card: Card, ease: int) -> None:
|
|
||||||
assert 1 <= ease <= 4
|
|
||||||
assert 0 <= card.queue <= 4
|
|
||||||
|
|
||||||
new_state = self._answerCard(card, ease)
|
|
||||||
|
|
||||||
self._handle_leech(card, new_state)
|
|
||||||
|
|
||||||
self.reps += 1
|
|
||||||
|
|
||||||
def _answerCard(self, card: Card, ease: int) -> _pb.SchedulingState:
|
|
||||||
states = self.col._backend.get_next_card_states(card.id)
|
|
||||||
if ease == BUTTON_ONE:
|
|
||||||
new_state = states.again
|
|
||||||
rating = _pb.AnswerCardIn.AGAIN
|
|
||||||
elif ease == BUTTON_TWO:
|
|
||||||
new_state = states.hard
|
|
||||||
rating = _pb.AnswerCardIn.HARD
|
|
||||||
elif ease == BUTTON_THREE:
|
|
||||||
new_state = states.good
|
|
||||||
rating = _pb.AnswerCardIn.GOOD
|
|
||||||
elif ease == BUTTON_FOUR:
|
|
||||||
new_state = states.easy
|
|
||||||
rating = _pb.AnswerCardIn.EASY
|
|
||||||
else:
|
|
||||||
assert False, "invalid ease"
|
|
||||||
|
|
||||||
self.col._backend.answer_card(
|
|
||||||
card_id=card.id,
|
|
||||||
current_state=states.current,
|
|
||||||
new_state=new_state,
|
|
||||||
rating=rating,
|
|
||||||
answered_at_millis=intTime(1000),
|
|
||||||
milliseconds_taken=card.timeTaken(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# fixme: tests assume card will be mutated, so we need to reload it
|
|
||||||
card.load()
|
|
||||||
|
|
||||||
return new_state
|
|
||||||
|
|
||||||
def _handle_leech(self, card: Card, new_state: _pb.SchedulingState) -> bool:
|
|
||||||
"True if was leech."
|
|
||||||
if self.col._backend.state_is_leech(new_state):
|
|
||||||
if hooks.card_did_leech.count() > 0:
|
|
||||||
hooks.card_did_leech(card)
|
|
||||||
# leech hooks assumed that card mutations would be saved for them
|
|
||||||
card.mod = intTime()
|
|
||||||
card.usn = self.col.usn()
|
|
||||||
card.flush()
|
|
||||||
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Next times
|
|
||||||
##########################################################################
|
|
||||||
# fixme: move these into tests_schedv2 in the future
|
|
||||||
|
|
||||||
def _interval_for_state(self, state: _pb.SchedulingState) -> int:
|
|
||||||
kind = state.WhichOneof("value")
|
|
||||||
if kind == "normal":
|
|
||||||
return self._interval_for_normal_state(state.normal)
|
|
||||||
elif kind == "filtered":
|
|
||||||
return self._interval_for_filtered_state(state.filtered)
|
|
||||||
else:
|
|
||||||
assert_exhaustive(kind)
|
|
||||||
return 0 # unreachable
|
|
||||||
|
|
||||||
def _interval_for_normal_state(self, normal: _pb.SchedulingState.Normal) -> int:
|
|
||||||
kind = normal.WhichOneof("value")
|
|
||||||
if kind == "new":
|
|
||||||
return 0
|
|
||||||
elif kind == "review":
|
|
||||||
return normal.review.scheduled_days * 86400
|
|
||||||
elif kind == "learning":
|
|
||||||
return normal.learning.scheduled_secs
|
|
||||||
elif kind == "relearning":
|
|
||||||
return normal.relearning.learning.scheduled_secs
|
|
||||||
else:
|
|
||||||
assert_exhaustive(kind)
|
|
||||||
return 0 # unreachable
|
|
||||||
|
|
||||||
def _interval_for_filtered_state(
|
|
||||||
self, filtered: _pb.SchedulingState.Filtered
|
|
||||||
) -> int:
|
|
||||||
kind = filtered.WhichOneof("value")
|
|
||||||
if kind == "preview":
|
|
||||||
return filtered.preview.scheduled_secs
|
|
||||||
elif kind == "rescheduling":
|
|
||||||
return self._interval_for_normal_state(filtered.rescheduling.original_state)
|
|
||||||
else:
|
|
||||||
assert_exhaustive(kind)
|
|
||||||
return 0 # unreachable
|
|
||||||
|
|
||||||
def nextIvl(self, card: Card, ease: int) -> Any:
|
|
||||||
"Don't use this - it is only required by tests, and will be moved in the future."
|
|
||||||
states = self.col._backend.get_next_card_states(card.id)
|
|
||||||
if ease == BUTTON_ONE:
|
|
||||||
new_state = states.again
|
|
||||||
elif ease == BUTTON_TWO:
|
|
||||||
new_state = states.hard
|
|
||||||
elif ease == BUTTON_THREE:
|
|
||||||
new_state = states.good
|
|
||||||
elif ease == BUTTON_FOUR:
|
|
||||||
new_state = states.easy
|
|
||||||
else:
|
|
||||||
assert False, "invalid ease"
|
|
||||||
|
|
||||||
return self._interval_for_state(new_state)
|
|
||||||
|
|
||||||
# Review-related UI helpers
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def countIdx(self, card: Card) -> int:
|
|
||||||
if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW):
|
|
||||||
return QUEUE_TYPE_LRN
|
|
||||||
return card.queue
|
|
||||||
|
|
||||||
def answerButtons(self, card: Card) -> int:
|
|
||||||
return 4
|
|
||||||
|
|
||||||
def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:
|
|
||||||
"Return the next interval for CARD as a string."
|
|
||||||
states = self.col._backend.get_next_card_states(card.id)
|
|
||||||
return self.col._backend.describe_next_states(states)[ease - 1]
|
|
||||||
|
|
||||||
# Deck list
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
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=intTime())
|
|
||||||
|
|
||||||
# Deck finished state & custom study
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def congratulations_info(self) -> CongratsInfo:
|
|
||||||
return self.col._backend.congrats_info()
|
|
||||||
|
|
||||||
def haveBuriedSiblings(self) -> bool:
|
|
||||||
return self.congratulations_info().have_sched_buried
|
|
||||||
|
|
||||||
def haveManuallyBuried(self) -> bool:
|
|
||||||
return self.congratulations_info().have_user_buried
|
|
||||||
|
|
||||||
def haveBuried(self) -> bool:
|
|
||||||
info = self.congratulations_info()
|
|
||||||
return info.have_sched_buried or info.have_user_buried
|
|
||||||
|
|
||||||
def extendLimits(self, new: int, rev: int) -> None:
|
|
||||||
did = self.col.decks.current()["id"]
|
|
||||||
self.col._backend.extend_limits(deck_id=did, new_delta=new, review_delta=rev)
|
|
||||||
|
|
||||||
# fixme: used by custom study
|
|
||||||
def totalRevForCurrentDeck(self) -> int:
|
|
||||||
return self.col.db.scalar(
|
|
||||||
f"""
|
|
||||||
select count() from cards where id in (
|
|
||||||
select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit 9999)"""
|
|
||||||
% self._deckLimit(),
|
|
||||||
self.today,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filtered deck handling
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def rebuild_filtered_deck(self, deck_id: int) -> int:
|
|
||||||
return self.col._backend.rebuild_filtered_deck(deck_id)
|
|
||||||
|
|
||||||
def empty_filtered_deck(self, deck_id: int) -> None:
|
|
||||||
self.col._backend.empty_filtered_deck(deck_id)
|
|
||||||
|
|
||||||
# Suspending & burying
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def unsuspend_cards(self, ids: List[int]) -> None:
|
|
||||||
self.col._backend.restore_buried_and_suspended_cards(ids)
|
|
||||||
|
|
||||||
def unbury_cards(self, ids: List[int]) -> None:
|
|
||||||
self.col._backend.restore_buried_and_suspended_cards(ids)
|
|
||||||
|
|
||||||
def unbury_cards_in_current_deck(
|
|
||||||
self,
|
|
||||||
mode: UnburyCurrentDeck.Mode.V = UnburyCurrentDeck.ALL,
|
|
||||||
) -> None:
|
|
||||||
self.col._backend.unbury_cards_in_current_deck(mode)
|
|
||||||
|
|
||||||
def suspend_cards(self, ids: Sequence[int]) -> None:
|
|
||||||
self.col._backend.bury_or_suspend_cards(
|
|
||||||
card_ids=ids, mode=BuryOrSuspend.SUSPEND
|
|
||||||
)
|
|
||||||
|
|
||||||
def bury_cards(self, ids: Sequence[int], manual: bool = True) -> None:
|
|
||||||
if manual:
|
|
||||||
mode = BuryOrSuspend.BURY_USER
|
|
||||||
else:
|
|
||||||
mode = BuryOrSuspend.BURY_SCHED
|
|
||||||
self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
|
|
||||||
|
|
||||||
def bury_note(self, note: Note) -> None:
|
|
||||||
self.bury_cards(note.card_ids())
|
|
||||||
|
|
||||||
# Resetting/rescheduling
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def schedule_cards_as_new(self, card_ids: List[int]) -> None:
|
|
||||||
"Put cards at the end of the new queue."
|
|
||||||
self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
|
|
||||||
|
|
||||||
def set_due_date(self, card_ids: List[int], days: str) -> None:
|
|
||||||
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
|
||||||
`days` can be of the form '5' or '5..7'"""
|
|
||||||
self.col._backend.set_due_date(card_ids=card_ids, days=days)
|
|
||||||
|
|
||||||
def resetCards(self, ids: List[int]) -> None:
|
|
||||||
"Completely reset cards for export."
|
|
||||||
sids = ids2str(ids)
|
|
||||||
# we want to avoid resetting due number of existing new cards on export
|
|
||||||
nonNew = self.col.db.list(
|
|
||||||
f"select id from cards where id in %s and (queue != {QUEUE_TYPE_NEW} or type != {CARD_TYPE_NEW})"
|
|
||||||
% sids
|
|
||||||
)
|
|
||||||
# reset all cards
|
|
||||||
self.col.db.execute(
|
|
||||||
f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_TYPE_NEW}"
|
|
||||||
" where id in %s" % sids
|
|
||||||
)
|
|
||||||
# and forget any non-new cards, changing their due numbers
|
|
||||||
self.col._backend.schedule_cards_as_new(card_ids=nonNew, log=False)
|
|
||||||
|
|
||||||
# Repositioning new cards
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def sortCards(
|
|
||||||
self,
|
|
||||||
cids: List[int],
|
|
||||||
start: int = 1,
|
|
||||||
step: int = 1,
|
|
||||||
shuffle: bool = False,
|
|
||||||
shift: bool = False,
|
|
||||||
) -> None:
|
|
||||||
self.col._backend.sort_cards(
|
|
||||||
card_ids=cids,
|
|
||||||
starting_from=start,
|
|
||||||
step_size=step,
|
|
||||||
randomize=shuffle,
|
|
||||||
shift_existing=shift,
|
|
||||||
)
|
|
||||||
|
|
||||||
def randomizeCards(self, did: int) -> None:
|
|
||||||
self.col._backend.sort_deck(deck_id=did, randomize=True)
|
|
||||||
|
|
||||||
def orderCards(self, did: int) -> None:
|
|
||||||
self.col._backend.sort_deck(deck_id=did, randomize=False)
|
|
||||||
|
|
||||||
def resortConf(self, conf: DeckConfig) -> None:
|
|
||||||
for did in self.col.decks.didsForConf(conf):
|
|
||||||
if conf["new"]["order"] == 0:
|
|
||||||
self.randomizeCards(did)
|
|
||||||
else:
|
|
||||||
self.orderCards(did)
|
|
||||||
|
|
||||||
# for post-import
|
|
||||||
def maybeRandomizeDeck(self, did: Optional[int] = None) -> None:
|
|
||||||
if not did:
|
|
||||||
did = self.col.decks.selected()
|
|
||||||
conf = self.col.decks.confForDid(did)
|
|
||||||
# in order due?
|
|
||||||
if conf["new"]["order"] == NEW_CARDS_RANDOM:
|
|
||||||
self.randomizeCards(did)
|
|
||||||
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
# unit tests
|
|
||||||
def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]:
|
|
||||||
return (ivl, ivl)
|
|
||||||
|
|
||||||
# Legacy aliases and helpers
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
# fixme: only used by totalRevForCurrentDeck and old deck stats
|
|
||||||
def _deckLimit(self) -> str:
|
|
||||||
self.col.decks.update_active()
|
|
||||||
return ids2str(self.col.decks.active())
|
|
||||||
|
|
||||||
def reschedCards(
|
|
||||||
self, card_ids: List[int], min_interval: int, max_interval: int
|
|
||||||
) -> None:
|
|
||||||
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
|
||||||
|
|
||||||
def buryNote(self, nid: int) -> None:
|
|
||||||
note = self.col.get_note(nid)
|
|
||||||
self.bury_cards(note.card_ids())
|
|
||||||
|
|
||||||
def unburyCards(self) -> None:
|
|
||||||
print(
|
|
||||||
"please use unbury_cards() or unbury_cards_in_current_deck instead of unburyCards()"
|
|
||||||
)
|
|
||||||
self.unbury_cards_in_current_deck()
|
|
||||||
|
|
||||||
def unburyCardsForDeck(self, type: str = "all") -> None:
|
|
||||||
print(
|
|
||||||
"please use unbury_cards_in_current_deck() instead of unburyCardsForDeck()"
|
|
||||||
)
|
|
||||||
if type == "all":
|
|
||||||
mode = UnburyCurrentDeck.ALL
|
|
||||||
elif type == "manual":
|
|
||||||
mode = UnburyCurrentDeck.USER_ONLY
|
|
||||||
else: # elif type == "siblings":
|
|
||||||
mode = UnburyCurrentDeck.SCHED_ONLY
|
|
||||||
self.unbury_cards_in_current_deck(mode)
|
|
||||||
|
|
||||||
def finishedMsg(self) -> str:
|
|
||||||
print("finishedMsg() is obsolete")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _nextDueMsg(self) -> str:
|
|
||||||
print("_nextDueMsg() is obsolete")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
|
|
||||||
did = did or self.col.decks.selected()
|
|
||||||
count = self.rebuild_filtered_deck(did) or None
|
|
||||||
if not count:
|
|
||||||
return None
|
|
||||||
# and change to our new deck
|
|
||||||
self.col.decks.select(did)
|
|
||||||
return count
|
|
||||||
|
|
||||||
def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
|
|
||||||
if lim is None:
|
|
||||||
self.empty_filtered_deck(did)
|
|
||||||
return
|
|
||||||
|
|
||||||
queue = f"""
|
|
||||||
queue = (case when queue < 0 then queue
|
|
||||||
when type in (1,{CARD_TYPE_RELEARNING}) then
|
|
||||||
(case when (case when odue then odue else due end) > 1000000000 then 1 else
|
|
||||||
{QUEUE_TYPE_DAY_LEARN_RELEARN} end)
|
|
||||||
else
|
|
||||||
type
|
|
||||||
end)
|
|
||||||
"""
|
|
||||||
self.col.db.execute(
|
|
||||||
"""
|
|
||||||
update cards set did = odid, %s,
|
|
||||||
due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? where %s"""
|
|
||||||
% (queue, lim),
|
|
||||||
self.col.usn(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def remFromDyn(self, cids: List[int]) -> None:
|
|
||||||
self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
|
|
||||||
|
|
||||||
def update_stats(
|
|
||||||
self,
|
|
||||||
deck_id: int,
|
|
||||||
new_delta: int = 0,
|
|
||||||
review_delta: int = 0,
|
|
||||||
milliseconds_delta: int = 0,
|
|
||||||
) -> None:
|
|
||||||
self.col._backend.update_stats(
|
|
||||||
deck_id=deck_id,
|
|
||||||
new_delta=new_delta,
|
|
||||||
review_delta=review_delta,
|
|
||||||
millisecond_delta=milliseconds_delta,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None:
|
|
||||||
did = card.did
|
|
||||||
if type == "new":
|
|
||||||
self.update_stats(did, new_delta=cnt)
|
|
||||||
elif type == "rev":
|
|
||||||
self.update_stats(did, review_delta=cnt)
|
|
||||||
elif type == "time":
|
|
||||||
self.update_stats(did, milliseconds_delta=cnt)
|
|
||||||
|
|
||||||
def deckDueTree(self) -> List:
|
|
||||||
"List of (base name, did, rev, lrn, new, children)"
|
|
||||||
print(
|
|
||||||
"deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()"
|
|
||||||
)
|
|
||||||
return from_json_bytes(self.col._backend.deck_tree_legacy())[5]
|
|
||||||
|
|
||||||
def _cardConf(self, card: Card) -> DeckConfig:
|
|
||||||
return self.col.decks.confForDid(card.did)
|
|
||||||
|
|
||||||
def _home_config(self, card: Card) -> DeckConfig:
|
|
||||||
return self.col.decks.confForDid(card.odid or card.did)
|
|
||||||
|
|
||||||
def _newConf(self, card: Card) -> QueueConfig:
|
|
||||||
return self._home_config(card)["new"]
|
|
||||||
|
|
||||||
def _lapseConf(self, card: Card) -> QueueConfig:
|
|
||||||
return self._home_config(card)["lapse"]
|
|
||||||
|
|
||||||
def _revConf(self, card: Card) -> QueueConfig:
|
|
||||||
return self._home_config(card)["rev"]
|
|
||||||
|
|
||||||
def _lrnConf(self, card: Card) -> QueueConfig:
|
|
||||||
if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING):
|
|
||||||
return self._lapseConf(card)
|
|
||||||
else:
|
|
||||||
return self._newConf(card)
|
|
||||||
|
|
||||||
unsuspendCards = unsuspend_cards
|
|
||||||
buryCards = bury_cards
|
|
||||||
suspendCards = suspend_cards
|
|
||||||
forgetCards = schedule_cards_as_new
|
|
10
pylib/anki/scheduler/__init__.py
Normal file
10
pylib/anki/scheduler/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import anki.scheduler.base as _base
|
||||||
|
|
||||||
|
UnburyCurrentDeck = _base.UnburyCurrentDeck
|
||||||
|
CongratsInfo = _base.CongratsInfo
|
||||||
|
BuryOrSuspend = _base.BuryOrSuspend
|
192
pylib/anki/scheduler/base.py
Normal file
192
pylib/anki/scheduler/base.py
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import anki
|
||||||
|
import anki._backend.backend_pb2 as _pb
|
||||||
|
|
||||||
|
SchedTimingToday = _pb.SchedTimingTodayOut
|
||||||
|
|
||||||
|
|
||||||
|
from typing import List, Optional, Sequence
|
||||||
|
|
||||||
|
from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV
|
||||||
|
from anki.decks import DeckConfig, DeckTreeNode
|
||||||
|
from anki.notes import Note
|
||||||
|
from anki.utils import ids2str, intTime
|
||||||
|
|
||||||
|
CongratsInfo = _pb.CongratsInfoOut
|
||||||
|
UnburyCurrentDeck = _pb.UnburyCardsInCurrentDeckIn
|
||||||
|
BuryOrSuspend = _pb.BuryOrSuspendCardsIn
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerBase:
|
||||||
|
"Actions shared between schedulers."
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
def __init__(self, col: anki.collection.Collection) -> None:
|
||||||
|
self.col = col.weakref()
|
||||||
|
|
||||||
|
def _timing_today(self) -> SchedTimingToday:
|
||||||
|
return self.col._backend.sched_timing_today()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def today(self) -> int:
|
||||||
|
return self._timing_today().days_elapsed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dayCutoff(self) -> int:
|
||||||
|
return self._timing_today().next_day_at
|
||||||
|
|
||||||
|
# Deck list
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
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=intTime())
|
||||||
|
|
||||||
|
# Deck finished state & custom study
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def congratulations_info(self) -> CongratsInfo:
|
||||||
|
return self.col._backend.congrats_info()
|
||||||
|
|
||||||
|
def haveBuriedSiblings(self) -> bool:
|
||||||
|
return self.congratulations_info().have_sched_buried
|
||||||
|
|
||||||
|
def haveManuallyBuried(self) -> bool:
|
||||||
|
return self.congratulations_info().have_user_buried
|
||||||
|
|
||||||
|
def haveBuried(self) -> bool:
|
||||||
|
info = self.congratulations_info()
|
||||||
|
return info.have_sched_buried or info.have_user_buried
|
||||||
|
|
||||||
|
def extendLimits(self, new: int, rev: int) -> None:
|
||||||
|
did = self.col.decks.current()["id"]
|
||||||
|
self.col._backend.extend_limits(deck_id=did, new_delta=new, review_delta=rev)
|
||||||
|
|
||||||
|
# fixme: used by custom study
|
||||||
|
def totalRevForCurrentDeck(self) -> int:
|
||||||
|
return self.col.db.scalar(
|
||||||
|
f"""
|
||||||
|
select count() from cards where id in (
|
||||||
|
select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit 9999)"""
|
||||||
|
% self._deckLimit(),
|
||||||
|
self.today,
|
||||||
|
)
|
||||||
|
|
||||||
|
# fixme: only used by totalRevForCurrentDeck and old deck stats;
|
||||||
|
# schedv2 defines separate version
|
||||||
|
def _deckLimit(self) -> str:
|
||||||
|
self.col.decks.update_active()
|
||||||
|
return ids2str(self.col.decks.active())
|
||||||
|
|
||||||
|
# Filtered deck handling
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def rebuild_filtered_deck(self, deck_id: int) -> int:
|
||||||
|
return self.col._backend.rebuild_filtered_deck(deck_id)
|
||||||
|
|
||||||
|
def empty_filtered_deck(self, deck_id: int) -> None:
|
||||||
|
self.col._backend.empty_filtered_deck(deck_id)
|
||||||
|
|
||||||
|
# Suspending & burying
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def unsuspend_cards(self, ids: List[int]) -> None:
|
||||||
|
self.col._backend.restore_buried_and_suspended_cards(ids)
|
||||||
|
|
||||||
|
def unbury_cards(self, ids: List[int]) -> None:
|
||||||
|
self.col._backend.restore_buried_and_suspended_cards(ids)
|
||||||
|
|
||||||
|
def unbury_cards_in_current_deck(
|
||||||
|
self,
|
||||||
|
mode: UnburyCurrentDeck.Mode.V = UnburyCurrentDeck.ALL,
|
||||||
|
) -> None:
|
||||||
|
self.col._backend.unbury_cards_in_current_deck(mode)
|
||||||
|
|
||||||
|
def suspend_cards(self, ids: Sequence[int]) -> None:
|
||||||
|
self.col._backend.bury_or_suspend_cards(
|
||||||
|
card_ids=ids, mode=BuryOrSuspend.SUSPEND
|
||||||
|
)
|
||||||
|
|
||||||
|
def bury_cards(self, ids: Sequence[int], manual: bool = True) -> None:
|
||||||
|
if manual:
|
||||||
|
mode = BuryOrSuspend.BURY_USER
|
||||||
|
else:
|
||||||
|
mode = BuryOrSuspend.BURY_SCHED
|
||||||
|
self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
|
||||||
|
|
||||||
|
def bury_note(self, note: Note) -> None:
|
||||||
|
self.bury_cards(note.card_ids())
|
||||||
|
|
||||||
|
# Resetting/rescheduling
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def schedule_cards_as_new(self, card_ids: List[int]) -> None:
|
||||||
|
"Put cards at the end of the new queue."
|
||||||
|
self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
|
||||||
|
|
||||||
|
def set_due_date(self, card_ids: List[int], days: str) -> None:
|
||||||
|
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
||||||
|
`days` can be of the form '5' or '5..7'"""
|
||||||
|
self.col._backend.set_due_date(card_ids=card_ids, days=days)
|
||||||
|
|
||||||
|
def resetCards(self, ids: List[int]) -> None:
|
||||||
|
"Completely reset cards for export."
|
||||||
|
sids = ids2str(ids)
|
||||||
|
# we want to avoid resetting due number of existing new cards on export
|
||||||
|
nonNew = self.col.db.list(
|
||||||
|
f"select id from cards where id in %s and (queue != {QUEUE_TYPE_NEW} or type != {CARD_TYPE_NEW})"
|
||||||
|
% sids
|
||||||
|
)
|
||||||
|
# reset all cards
|
||||||
|
self.col.db.execute(
|
||||||
|
f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_TYPE_NEW}"
|
||||||
|
" where id in %s" % sids
|
||||||
|
)
|
||||||
|
# and forget any non-new cards, changing their due numbers
|
||||||
|
self.col._backend.schedule_cards_as_new(card_ids=nonNew, log=False)
|
||||||
|
|
||||||
|
# Repositioning new cards
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def sortCards(
|
||||||
|
self,
|
||||||
|
cids: List[int],
|
||||||
|
start: int = 1,
|
||||||
|
step: int = 1,
|
||||||
|
shuffle: bool = False,
|
||||||
|
shift: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.col._backend.sort_cards(
|
||||||
|
card_ids=cids,
|
||||||
|
starting_from=start,
|
||||||
|
step_size=step,
|
||||||
|
randomize=shuffle,
|
||||||
|
shift_existing=shift,
|
||||||
|
)
|
||||||
|
|
||||||
|
def randomizeCards(self, did: int) -> None:
|
||||||
|
self.col._backend.sort_deck(deck_id=did, randomize=True)
|
||||||
|
|
||||||
|
def orderCards(self, did: int) -> None:
|
||||||
|
self.col._backend.sort_deck(deck_id=did, randomize=False)
|
||||||
|
|
||||||
|
def resortConf(self, conf: DeckConfig) -> None:
|
||||||
|
for did in self.col.decks.didsForConf(conf):
|
||||||
|
if conf["new"]["order"] == 0:
|
||||||
|
self.randomizeCards(did)
|
||||||
|
else:
|
||||||
|
self.orderCards(did)
|
||||||
|
|
||||||
|
# for post-import
|
||||||
|
def maybeRandomizeDeck(self, did: Optional[int] = None) -> None:
|
||||||
|
if not did:
|
||||||
|
did = self.col.decks.selected()
|
||||||
|
conf = self.col.decks.confForDid(did)
|
||||||
|
# in order due?
|
||||||
|
if conf["new"]["order"] == NEW_CARDS_RANDOM:
|
||||||
|
self.randomizeCards(did)
|
148
pylib/anki/scheduler/legacy.py
Normal file
148
pylib/anki/scheduler/legacy.py
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from anki.cards import Card
|
||||||
|
from anki.consts import (
|
||||||
|
CARD_TYPE_RELEARNING,
|
||||||
|
CARD_TYPE_REV,
|
||||||
|
QUEUE_TYPE_DAY_LEARN_RELEARN,
|
||||||
|
)
|
||||||
|
from anki.decks import DeckConfig, QueueConfig
|
||||||
|
from anki.scheduler.base import SchedulerBase, UnburyCurrentDeck
|
||||||
|
from anki.utils import from_json_bytes, ids2str
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerBaseWithLegacy(SchedulerBase):
|
||||||
|
"Legacy aliases and helpers. These will go away in the future."
|
||||||
|
|
||||||
|
def reschedCards(
|
||||||
|
self, card_ids: List[int], min_interval: int, max_interval: int
|
||||||
|
) -> None:
|
||||||
|
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
||||||
|
|
||||||
|
def buryNote(self, nid: int) -> None:
|
||||||
|
note = self.col.get_note(nid)
|
||||||
|
self.bury_cards(note.card_ids())
|
||||||
|
|
||||||
|
def unburyCards(self) -> None:
|
||||||
|
print(
|
||||||
|
"please use unbury_cards() or unbury_cards_in_current_deck instead of unburyCards()"
|
||||||
|
)
|
||||||
|
self.unbury_cards_in_current_deck()
|
||||||
|
|
||||||
|
def unburyCardsForDeck(self, type: str = "all") -> None:
|
||||||
|
print(
|
||||||
|
"please use unbury_cards_in_current_deck() instead of unburyCardsForDeck()"
|
||||||
|
)
|
||||||
|
if type == "all":
|
||||||
|
mode = UnburyCurrentDeck.ALL
|
||||||
|
elif type == "manual":
|
||||||
|
mode = UnburyCurrentDeck.USER_ONLY
|
||||||
|
else: # elif type == "siblings":
|
||||||
|
mode = UnburyCurrentDeck.SCHED_ONLY
|
||||||
|
self.unbury_cards_in_current_deck(mode)
|
||||||
|
|
||||||
|
def finishedMsg(self) -> str:
|
||||||
|
print("finishedMsg() is obsolete")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _nextDueMsg(self) -> str:
|
||||||
|
print("_nextDueMsg() is obsolete")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
|
||||||
|
did = did or self.col.decks.selected()
|
||||||
|
count = self.rebuild_filtered_deck(did) or None
|
||||||
|
if not count:
|
||||||
|
return None
|
||||||
|
# and change to our new deck
|
||||||
|
self.col.decks.select(did)
|
||||||
|
return count
|
||||||
|
|
||||||
|
def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
|
||||||
|
if lim is None:
|
||||||
|
self.empty_filtered_deck(did)
|
||||||
|
return
|
||||||
|
|
||||||
|
queue = f"""
|
||||||
|
queue = (case when queue < 0 then queue
|
||||||
|
when type in (1,{CARD_TYPE_RELEARNING}) then
|
||||||
|
(case when (case when odue then odue else due end) > 1000000000 then 1 else
|
||||||
|
{QUEUE_TYPE_DAY_LEARN_RELEARN} end)
|
||||||
|
else
|
||||||
|
type
|
||||||
|
end)
|
||||||
|
"""
|
||||||
|
self.col.db.execute(
|
||||||
|
"""
|
||||||
|
update cards set did = odid, %s,
|
||||||
|
due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? where %s"""
|
||||||
|
% (queue, lim),
|
||||||
|
self.col.usn(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def remFromDyn(self, cids: List[int]) -> None:
|
||||||
|
self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
|
||||||
|
|
||||||
|
# used by v2 scheduler and some add-ons
|
||||||
|
def update_stats(
|
||||||
|
self,
|
||||||
|
deck_id: int,
|
||||||
|
new_delta: int = 0,
|
||||||
|
review_delta: int = 0,
|
||||||
|
milliseconds_delta: int = 0,
|
||||||
|
) -> None:
|
||||||
|
self.col._backend.update_stats(
|
||||||
|
deck_id=deck_id,
|
||||||
|
new_delta=new_delta,
|
||||||
|
review_delta=review_delta,
|
||||||
|
millisecond_delta=milliseconds_delta,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None:
|
||||||
|
did = card.did
|
||||||
|
if type == "new":
|
||||||
|
self.update_stats(did, new_delta=cnt)
|
||||||
|
elif type == "rev":
|
||||||
|
self.update_stats(did, review_delta=cnt)
|
||||||
|
elif type == "time":
|
||||||
|
self.update_stats(did, milliseconds_delta=cnt)
|
||||||
|
|
||||||
|
def deckDueTree(self) -> List:
|
||||||
|
"List of (base name, did, rev, lrn, new, children)"
|
||||||
|
print(
|
||||||
|
"deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()"
|
||||||
|
)
|
||||||
|
return from_json_bytes(self.col._backend.deck_tree_legacy())[5]
|
||||||
|
|
||||||
|
# unit tests
|
||||||
|
def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]:
|
||||||
|
return (ivl, ivl)
|
||||||
|
|
||||||
|
def _cardConf(self, card: Card) -> DeckConfig:
|
||||||
|
return self.col.decks.confForDid(card.did)
|
||||||
|
|
||||||
|
def _home_config(self, card: Card) -> DeckConfig:
|
||||||
|
return self.col.decks.confForDid(card.odid or card.did)
|
||||||
|
|
||||||
|
def _newConf(self, card: Card) -> QueueConfig:
|
||||||
|
return self._home_config(card)["new"]
|
||||||
|
|
||||||
|
def _lapseConf(self, card: Card) -> QueueConfig:
|
||||||
|
return self._home_config(card)["lapse"]
|
||||||
|
|
||||||
|
def _revConf(self, card: Card) -> QueueConfig:
|
||||||
|
return self._home_config(card)["rev"]
|
||||||
|
|
||||||
|
def _lrnConf(self, card: Card) -> QueueConfig:
|
||||||
|
if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING):
|
||||||
|
return self._lapseConf(card)
|
||||||
|
else:
|
||||||
|
return self._newConf(card)
|
||||||
|
|
||||||
|
unsuspendCards = SchedulerBase.unsuspend_cards
|
||||||
|
buryCards = SchedulerBase.bury_cards
|
||||||
|
suspendCards = SchedulerBase.suspend_cards
|
||||||
|
forgetCards = SchedulerBase.schedule_cards_as_new
|
225
pylib/anki/scheduler/v3.py
Normal file
225
pylib/anki/scheduler/v3.py
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file contains experimental scheduler changes, and is not currently
|
||||||
|
used by Anki.
|
||||||
|
|
||||||
|
It uses the same DB schema as the V2 scheduler, and 'schedVer' remains
|
||||||
|
as '2' internally.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Tuple, Union
|
||||||
|
|
||||||
|
import anki._backend.backend_pb2 as _pb
|
||||||
|
from anki import hooks
|
||||||
|
from anki.cards import Card
|
||||||
|
from anki.consts import *
|
||||||
|
from anki.scheduler.base import CongratsInfo
|
||||||
|
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
||||||
|
from anki.types import assert_exhaustive
|
||||||
|
from anki.utils import intTime
|
||||||
|
|
||||||
|
QueuedCards = _pb.GetQueuedCardsOut.QueuedCards
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler(SchedulerBaseWithLegacy):
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
# don't rely on this, it will likely be removed in the future
|
||||||
|
reps = 0
|
||||||
|
|
||||||
|
# Fetching the next card
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
# backend automatically resets queues as operations are performed
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_queued_cards(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
fetch_limit: int = 1,
|
||||||
|
intraday_learning_only: bool = False,
|
||||||
|
) -> Union[QueuedCards, CongratsInfo]:
|
||||||
|
info = self.col._backend.get_queued_cards(
|
||||||
|
fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only
|
||||||
|
)
|
||||||
|
kind = info.WhichOneof("value")
|
||||||
|
if kind == "queued_cards":
|
||||||
|
return info.queued_cards
|
||||||
|
elif kind == "congrats_info":
|
||||||
|
return info.congrats_info
|
||||||
|
else:
|
||||||
|
assert_exhaustive(kind)
|
||||||
|
assert False
|
||||||
|
|
||||||
|
def getCard(self) -> Optional[Card]:
|
||||||
|
"""Fetch the next card from the queue. None if finished."""
|
||||||
|
response = self.get_queued_cards()
|
||||||
|
if isinstance(response, QueuedCards):
|
||||||
|
backend_card = response.cards[0].card
|
||||||
|
card = Card(self.col)
|
||||||
|
card._load_from_backend_card(backend_card)
|
||||||
|
card.startTimer()
|
||||||
|
return card
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _is_finished(self) -> bool:
|
||||||
|
"Don't use this, it is a stop-gap until this code is refactored."
|
||||||
|
info = self.get_queued_cards()
|
||||||
|
return isinstance(info, CongratsInfo)
|
||||||
|
|
||||||
|
def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
|
||||||
|
info = self.get_queued_cards()
|
||||||
|
if isinstance(info, CongratsInfo):
|
||||||
|
counts = [0, 0, 0]
|
||||||
|
else:
|
||||||
|
counts = [info.new_count, info.learning_count, info.review_count]
|
||||||
|
|
||||||
|
return tuple(counts) # type: ignore
|
||||||
|
|
||||||
|
@property
|
||||||
|
def newCount(self) -> int:
|
||||||
|
return self.counts()[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lrnCount(self) -> int:
|
||||||
|
return self.counts()[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reviewCount(self) -> int:
|
||||||
|
return self.counts()[2]
|
||||||
|
|
||||||
|
# Answering a card
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def answerCard(self, card: Card, ease: int) -> None:
|
||||||
|
assert 1 <= ease <= 4
|
||||||
|
assert 0 <= card.queue <= 4
|
||||||
|
|
||||||
|
new_state = self._answerCard(card, ease)
|
||||||
|
|
||||||
|
self._handle_leech(card, new_state)
|
||||||
|
|
||||||
|
self.reps += 1
|
||||||
|
|
||||||
|
def _answerCard(self, card: Card, ease: int) -> _pb.SchedulingState:
|
||||||
|
states = self.col._backend.get_next_card_states(card.id)
|
||||||
|
if ease == BUTTON_ONE:
|
||||||
|
new_state = states.again
|
||||||
|
rating = _pb.AnswerCardIn.AGAIN
|
||||||
|
elif ease == BUTTON_TWO:
|
||||||
|
new_state = states.hard
|
||||||
|
rating = _pb.AnswerCardIn.HARD
|
||||||
|
elif ease == BUTTON_THREE:
|
||||||
|
new_state = states.good
|
||||||
|
rating = _pb.AnswerCardIn.GOOD
|
||||||
|
elif ease == BUTTON_FOUR:
|
||||||
|
new_state = states.easy
|
||||||
|
rating = _pb.AnswerCardIn.EASY
|
||||||
|
else:
|
||||||
|
assert False, "invalid ease"
|
||||||
|
|
||||||
|
self.col._backend.answer_card(
|
||||||
|
card_id=card.id,
|
||||||
|
current_state=states.current,
|
||||||
|
new_state=new_state,
|
||||||
|
rating=rating,
|
||||||
|
answered_at_millis=intTime(1000),
|
||||||
|
milliseconds_taken=card.timeTaken(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# fixme: tests assume card will be mutated, so we need to reload it
|
||||||
|
card.load()
|
||||||
|
|
||||||
|
return new_state
|
||||||
|
|
||||||
|
def _handle_leech(self, card: Card, new_state: _pb.SchedulingState) -> bool:
|
||||||
|
"True if was leech."
|
||||||
|
if self.col._backend.state_is_leech(new_state):
|
||||||
|
if hooks.card_did_leech.count() > 0:
|
||||||
|
hooks.card_did_leech(card)
|
||||||
|
# leech hooks assumed that card mutations would be saved for them
|
||||||
|
card.mod = intTime()
|
||||||
|
card.usn = self.col.usn()
|
||||||
|
card.flush()
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Next times
|
||||||
|
##########################################################################
|
||||||
|
# fixme: move these into tests_schedv2 in the future
|
||||||
|
|
||||||
|
def _interval_for_state(self, state: _pb.SchedulingState) -> int:
|
||||||
|
kind = state.WhichOneof("value")
|
||||||
|
if kind == "normal":
|
||||||
|
return self._interval_for_normal_state(state.normal)
|
||||||
|
elif kind == "filtered":
|
||||||
|
return self._interval_for_filtered_state(state.filtered)
|
||||||
|
else:
|
||||||
|
assert_exhaustive(kind)
|
||||||
|
return 0 # unreachable
|
||||||
|
|
||||||
|
def _interval_for_normal_state(self, normal: _pb.SchedulingState.Normal) -> int:
|
||||||
|
kind = normal.WhichOneof("value")
|
||||||
|
if kind == "new":
|
||||||
|
return 0
|
||||||
|
elif kind == "review":
|
||||||
|
return normal.review.scheduled_days * 86400
|
||||||
|
elif kind == "learning":
|
||||||
|
return normal.learning.scheduled_secs
|
||||||
|
elif kind == "relearning":
|
||||||
|
return normal.relearning.learning.scheduled_secs
|
||||||
|
else:
|
||||||
|
assert_exhaustive(kind)
|
||||||
|
return 0 # unreachable
|
||||||
|
|
||||||
|
def _interval_for_filtered_state(
|
||||||
|
self, filtered: _pb.SchedulingState.Filtered
|
||||||
|
) -> int:
|
||||||
|
kind = filtered.WhichOneof("value")
|
||||||
|
if kind == "preview":
|
||||||
|
return filtered.preview.scheduled_secs
|
||||||
|
elif kind == "rescheduling":
|
||||||
|
return self._interval_for_normal_state(filtered.rescheduling.original_state)
|
||||||
|
else:
|
||||||
|
assert_exhaustive(kind)
|
||||||
|
return 0 # unreachable
|
||||||
|
|
||||||
|
def nextIvl(self, card: Card, ease: int) -> Any:
|
||||||
|
"Don't use this - it is only required by tests, and will be moved in the future."
|
||||||
|
states = self.col._backend.get_next_card_states(card.id)
|
||||||
|
if ease == BUTTON_ONE:
|
||||||
|
new_state = states.again
|
||||||
|
elif ease == BUTTON_TWO:
|
||||||
|
new_state = states.hard
|
||||||
|
elif ease == BUTTON_THREE:
|
||||||
|
new_state = states.good
|
||||||
|
elif ease == BUTTON_FOUR:
|
||||||
|
new_state = states.easy
|
||||||
|
else:
|
||||||
|
assert False, "invalid ease"
|
||||||
|
|
||||||
|
return self._interval_for_state(new_state)
|
||||||
|
|
||||||
|
# Review-related UI helpers
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
def countIdx(self, card: Card) -> int:
|
||||||
|
if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW):
|
||||||
|
return QUEUE_TYPE_LRN
|
||||||
|
return card.queue
|
||||||
|
|
||||||
|
def answerButtons(self, card: Card) -> int:
|
||||||
|
return 4
|
||||||
|
|
||||||
|
def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:
|
||||||
|
"Return the next interval for CARD as a string."
|
||||||
|
states = self.col._backend.get_next_card_states(card.id)
|
||||||
|
return self.col._backend.describe_next_states(states)[ease - 1]
|
|
@ -3,27 +3,23 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pprint
|
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from heapq import *
|
from heapq import *
|
||||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki._backend.backend_pb2 as _pb
|
import anki._backend.backend_pb2 as _pb
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.decks import Deck, DeckConfig, DeckTreeNode, QueueConfig
|
from anki.decks import Deck, DeckConfig, QueueConfig
|
||||||
from anki.lang import FormatTimeSpan
|
from anki.lang import FormatTimeSpan
|
||||||
from anki.notes import Note
|
from anki.scheduler.legacy import SchedulerBaseWithLegacy
|
||||||
from anki.utils import from_json_bytes, ids2str, intTime
|
from anki.utils import ids2str, intTime
|
||||||
|
|
||||||
CongratsInfo = _pb.CongratsInfoOut
|
|
||||||
CountsForDeckToday = _pb.CountsForDeckTodayOut
|
CountsForDeckToday = _pb.CountsForDeckTodayOut
|
||||||
SchedTimingToday = _pb.SchedTimingTodayOut
|
SchedTimingToday = _pb.SchedTimingTodayOut
|
||||||
UnburyCurrentDeck = _pb.UnburyCardsInCurrentDeckIn
|
|
||||||
BuryOrSuspend = _pb.BuryOrSuspendCardsIn
|
|
||||||
|
|
||||||
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
||||||
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
|
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
|
||||||
|
@ -34,20 +30,19 @@ BuryOrSuspend = _pb.BuryOrSuspendCardsIn
|
||||||
# odue/odid store original due/did when cards moved to filtered deck
|
# odue/odid store original due/did when cards moved to filtered deck
|
||||||
|
|
||||||
|
|
||||||
class Scheduler:
|
class Scheduler(SchedulerBaseWithLegacy):
|
||||||
|
version = 2
|
||||||
name = "std2"
|
name = "std2"
|
||||||
haveCustomStudy = True
|
haveCustomStudy = True
|
||||||
_burySiblingsOnAnswer = True
|
_burySiblingsOnAnswer = True
|
||||||
revCount: int
|
revCount: int
|
||||||
is_2021 = False
|
|
||||||
|
|
||||||
def __init__(self, col: anki.collection.Collection) -> None:
|
def __init__(self, col: anki.collection.Collection) -> None:
|
||||||
self.col = col.weakref()
|
super().__init__(col)
|
||||||
self.queueLimit = 50
|
self.queueLimit = 50
|
||||||
self.reportLimit = 1000
|
self.reportLimit = 1000
|
||||||
self.dynReportLimit = 99999
|
self.dynReportLimit = 99999
|
||||||
self.reps = 0
|
self.reps = 0
|
||||||
self.today: Optional[int] = None
|
|
||||||
self._haveQueues = False
|
self._haveQueues = False
|
||||||
self._lrnCutoff = 0
|
self._lrnCutoff = 0
|
||||||
self._updateCutoff()
|
self._updateCutoff()
|
||||||
|
@ -56,18 +51,13 @@ class Scheduler:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _updateCutoff(self) -> None:
|
def _updateCutoff(self) -> None:
|
||||||
timing = self._timing_today()
|
pass
|
||||||
self.today = timing.days_elapsed
|
|
||||||
self.dayCutoff = timing.next_day_at
|
|
||||||
|
|
||||||
def _checkDay(self) -> None:
|
def _checkDay(self) -> None:
|
||||||
# check if the day has rolled over
|
# check if the day has rolled over
|
||||||
if time.time() > self.dayCutoff:
|
if time.time() > self.dayCutoff:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def _timing_today(self) -> SchedTimingToday:
|
|
||||||
return self.col._backend.sched_timing_today()
|
|
||||||
|
|
||||||
# Fetching the next card
|
# Fetching the next card
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -926,11 +916,11 @@ limit ?"""
|
||||||
min, max = self._fuzzIvlRange(ivl)
|
min, max = self._fuzzIvlRange(ivl)
|
||||||
return random.randint(min, max)
|
return random.randint(min, max)
|
||||||
|
|
||||||
def _fuzzIvlRange(self, ivl: int) -> List[int]:
|
def _fuzzIvlRange(self, ivl: int) -> Tuple[int, int]:
|
||||||
if ivl < 2:
|
if ivl < 2:
|
||||||
return [1, 1]
|
return (1, 1)
|
||||||
elif ivl == 2:
|
elif ivl == 2:
|
||||||
return [2, 3]
|
return (2, 3)
|
||||||
elif ivl < 7:
|
elif ivl < 7:
|
||||||
fuzz = int(ivl * 0.25)
|
fuzz = int(ivl * 0.25)
|
||||||
elif ivl < 30:
|
elif ivl < 30:
|
||||||
|
@ -939,7 +929,7 @@ limit ?"""
|
||||||
fuzz = max(4, int(ivl * 0.05))
|
fuzz = max(4, int(ivl * 0.05))
|
||||||
# fuzz at least a day
|
# fuzz at least a day
|
||||||
fuzz = max(fuzz, 1)
|
fuzz = max(fuzz, 1)
|
||||||
return [ivl - fuzz, ivl + fuzz]
|
return (ivl - fuzz, ivl + fuzz)
|
||||||
|
|
||||||
def _constrainedIvl(
|
def _constrainedIvl(
|
||||||
self, ivl: float, conf: QueueConfig, prev: int, fuzz: bool
|
self, ivl: float, conf: QueueConfig, prev: int, fuzz: bool
|
||||||
|
@ -1001,20 +991,6 @@ limit ?"""
|
||||||
# Daily limits
|
# Daily limits
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def update_stats(
|
|
||||||
self,
|
|
||||||
deck_id: int,
|
|
||||||
new_delta: int = 0,
|
|
||||||
review_delta: int = 0,
|
|
||||||
milliseconds_delta: int = 0,
|
|
||||||
) -> None:
|
|
||||||
self.col._backend.update_stats(
|
|
||||||
deck_id=deck_id,
|
|
||||||
new_delta=new_delta,
|
|
||||||
review_delta=review_delta,
|
|
||||||
millisecond_delta=milliseconds_delta,
|
|
||||||
)
|
|
||||||
|
|
||||||
def counts_for_deck_today(self, deck_id: int) -> CountsForDeckToday:
|
def counts_for_deck_today(self, deck_id: int) -> CountsForDeckToday:
|
||||||
return self.col._backend.counts_for_deck_today(deck_id)
|
return self.col._backend.counts_for_deck_today(deck_id)
|
||||||
|
|
||||||
|
@ -1157,252 +1133,6 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
||||||
s = "<" + s
|
s = "<" + s
|
||||||
return s
|
return s
|
||||||
|
|
||||||
# Deck list
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
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=intTime())
|
|
||||||
|
|
||||||
# Deck finished state & custom study
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def congratulations_info(self) -> CongratsInfo:
|
|
||||||
return self.col._backend.congrats_info()
|
|
||||||
|
|
||||||
def haveBuriedSiblings(self) -> bool:
|
|
||||||
return self.congratulations_info().have_sched_buried
|
|
||||||
|
|
||||||
def haveManuallyBuried(self) -> bool:
|
|
||||||
return self.congratulations_info().have_user_buried
|
|
||||||
|
|
||||||
def haveBuried(self) -> bool:
|
|
||||||
info = self.congratulations_info()
|
|
||||||
return info.have_sched_buried or info.have_user_buried
|
|
||||||
|
|
||||||
def extendLimits(self, new: int, rev: int) -> None:
|
|
||||||
did = self.col.decks.current()["id"]
|
|
||||||
self.col._backend.extend_limits(deck_id=did, new_delta=new, review_delta=rev)
|
|
||||||
|
|
||||||
def _is_finished(self) -> bool:
|
def _is_finished(self) -> bool:
|
||||||
"Don't use this, it is a stop-gap until this code is refactored."
|
"Don't use this, it is a stop-gap until this code is refactored."
|
||||||
return not any((self.newCount, self.revCount, self._immediate_learn_count))
|
return not any((self.newCount, self.revCount, self._immediate_learn_count))
|
||||||
|
|
||||||
def totalRevForCurrentDeck(self) -> int:
|
|
||||||
return self.col.db.scalar(
|
|
||||||
f"""
|
|
||||||
select count() from cards where id in (
|
|
||||||
select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit ?)"""
|
|
||||||
% self._deckLimit(),
|
|
||||||
self.today,
|
|
||||||
self.reportLimit,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filtered deck handling
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def rebuild_filtered_deck(self, deck_id: int) -> int:
|
|
||||||
return self.col._backend.rebuild_filtered_deck(deck_id)
|
|
||||||
|
|
||||||
def empty_filtered_deck(self, deck_id: int) -> None:
|
|
||||||
self.col._backend.empty_filtered_deck(deck_id)
|
|
||||||
|
|
||||||
# Suspending & burying
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def unsuspend_cards(self, ids: List[int]) -> None:
|
|
||||||
self.col._backend.restore_buried_and_suspended_cards(ids)
|
|
||||||
|
|
||||||
def unbury_cards(self, ids: List[int]) -> None:
|
|
||||||
self.col._backend.restore_buried_and_suspended_cards(ids)
|
|
||||||
|
|
||||||
def unbury_cards_in_current_deck(
|
|
||||||
self,
|
|
||||||
mode: UnburyCurrentDeck.Mode.V = UnburyCurrentDeck.ALL,
|
|
||||||
) -> None:
|
|
||||||
self.col._backend.unbury_cards_in_current_deck(mode)
|
|
||||||
|
|
||||||
def suspend_cards(self, ids: Sequence[int]) -> None:
|
|
||||||
self.col._backend.bury_or_suspend_cards(
|
|
||||||
card_ids=ids, mode=BuryOrSuspend.SUSPEND
|
|
||||||
)
|
|
||||||
|
|
||||||
def bury_cards(self, ids: Sequence[int], manual: bool = True) -> None:
|
|
||||||
if manual:
|
|
||||||
mode = BuryOrSuspend.BURY_USER
|
|
||||||
else:
|
|
||||||
mode = BuryOrSuspend.BURY_SCHED
|
|
||||||
self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
|
|
||||||
|
|
||||||
def bury_note(self, note: Note) -> None:
|
|
||||||
self.bury_cards(note.card_ids())
|
|
||||||
|
|
||||||
# Resetting/rescheduling
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def schedule_cards_as_new(self, card_ids: List[int]) -> None:
|
|
||||||
"Put cards at the end of the new queue."
|
|
||||||
self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
|
|
||||||
|
|
||||||
def set_due_date(self, card_ids: List[int], days: str) -> None:
|
|
||||||
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
|
||||||
`days` can be of the form '5' or '5..7'"""
|
|
||||||
self.col._backend.set_due_date(card_ids=card_ids, days=days)
|
|
||||||
|
|
||||||
def resetCards(self, ids: List[int]) -> None:
|
|
||||||
"Completely reset cards for export."
|
|
||||||
sids = ids2str(ids)
|
|
||||||
# we want to avoid resetting due number of existing new cards on export
|
|
||||||
nonNew = self.col.db.list(
|
|
||||||
f"select id from cards where id in %s and (queue != {QUEUE_TYPE_NEW} or type != {CARD_TYPE_NEW})"
|
|
||||||
% sids
|
|
||||||
)
|
|
||||||
# reset all cards
|
|
||||||
self.col.db.execute(
|
|
||||||
f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_TYPE_NEW}"
|
|
||||||
" where id in %s" % sids
|
|
||||||
)
|
|
||||||
# and forget any non-new cards, changing their due numbers
|
|
||||||
self.col._backend.schedule_cards_as_new(card_ids=nonNew, log=False)
|
|
||||||
|
|
||||||
# Repositioning new cards
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def sortCards(
|
|
||||||
self,
|
|
||||||
cids: List[int],
|
|
||||||
start: int = 1,
|
|
||||||
step: int = 1,
|
|
||||||
shuffle: bool = False,
|
|
||||||
shift: bool = False,
|
|
||||||
) -> None:
|
|
||||||
self.col._backend.sort_cards(
|
|
||||||
card_ids=cids,
|
|
||||||
starting_from=start,
|
|
||||||
step_size=step,
|
|
||||||
randomize=shuffle,
|
|
||||||
shift_existing=shift,
|
|
||||||
)
|
|
||||||
|
|
||||||
def randomizeCards(self, did: int) -> None:
|
|
||||||
self.col._backend.sort_deck(deck_id=did, randomize=True)
|
|
||||||
|
|
||||||
def orderCards(self, did: int) -> None:
|
|
||||||
self.col._backend.sort_deck(deck_id=did, randomize=False)
|
|
||||||
|
|
||||||
def resortConf(self, conf: DeckConfig) -> None:
|
|
||||||
for did in self.col.decks.didsForConf(conf):
|
|
||||||
if conf["new"]["order"] == 0:
|
|
||||||
self.randomizeCards(did)
|
|
||||||
else:
|
|
||||||
self.orderCards(did)
|
|
||||||
|
|
||||||
# for post-import
|
|
||||||
def maybeRandomizeDeck(self, did: Optional[int] = None) -> None:
|
|
||||||
if not did:
|
|
||||||
did = self.col.decks.selected()
|
|
||||||
conf = self.col.decks.confForDid(did)
|
|
||||||
# in order due?
|
|
||||||
if conf["new"]["order"] == NEW_CARDS_RANDOM:
|
|
||||||
self.randomizeCards(did)
|
|
||||||
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
d = dict(self.__dict__)
|
|
||||||
del d["col"]
|
|
||||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
|
||||||
|
|
||||||
# Legacy aliases and helpers
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
def reschedCards(
|
|
||||||
self, card_ids: List[int], min_interval: int, max_interval: int
|
|
||||||
) -> None:
|
|
||||||
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
|
||||||
|
|
||||||
def buryNote(self, nid: int) -> None:
|
|
||||||
note = self.col.get_note(nid)
|
|
||||||
self.bury_cards(note.card_ids())
|
|
||||||
|
|
||||||
def unburyCards(self) -> None:
|
|
||||||
print(
|
|
||||||
"please use unbury_cards() or unbury_cards_in_current_deck instead of unburyCards()"
|
|
||||||
)
|
|
||||||
self.unbury_cards_in_current_deck()
|
|
||||||
|
|
||||||
def unburyCardsForDeck(self, type: str = "all") -> None:
|
|
||||||
print(
|
|
||||||
"please use unbury_cards_in_current_deck() instead of unburyCardsForDeck()"
|
|
||||||
)
|
|
||||||
if type == "all":
|
|
||||||
mode = UnburyCurrentDeck.ALL
|
|
||||||
elif type == "manual":
|
|
||||||
mode = UnburyCurrentDeck.USER_ONLY
|
|
||||||
else: # elif type == "siblings":
|
|
||||||
mode = UnburyCurrentDeck.SCHED_ONLY
|
|
||||||
self.unbury_cards_in_current_deck(mode)
|
|
||||||
|
|
||||||
def finishedMsg(self) -> str:
|
|
||||||
print("finishedMsg() is obsolete")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _nextDueMsg(self) -> str:
|
|
||||||
print("_nextDueMsg() is obsolete")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
|
|
||||||
did = did or self.col.decks.selected()
|
|
||||||
count = self.rebuild_filtered_deck(did) or None
|
|
||||||
if not count:
|
|
||||||
return None
|
|
||||||
# and change to our new deck
|
|
||||||
self.col.decks.select(did)
|
|
||||||
return count
|
|
||||||
|
|
||||||
def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
|
|
||||||
if lim is None:
|
|
||||||
self.empty_filtered_deck(did)
|
|
||||||
return
|
|
||||||
|
|
||||||
queue = f"""
|
|
||||||
queue = (case when queue < 0 then queue
|
|
||||||
when type in (1,{CARD_TYPE_RELEARNING}) then
|
|
||||||
(case when (case when odue then odue else due end) > 1000000000 then 1 else
|
|
||||||
{QUEUE_TYPE_DAY_LEARN_RELEARN} end)
|
|
||||||
else
|
|
||||||
type
|
|
||||||
end)
|
|
||||||
"""
|
|
||||||
self.col.db.execute(
|
|
||||||
"""
|
|
||||||
update cards set did = odid, %s,
|
|
||||||
due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? where %s"""
|
|
||||||
% (queue, lim),
|
|
||||||
self.col.usn(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def remFromDyn(self, cids: List[int]) -> None:
|
|
||||||
self.emptyDyn(None, f"id in {ids2str(cids)} and odid")
|
|
||||||
|
|
||||||
def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None:
|
|
||||||
did = card.did
|
|
||||||
if type == "new":
|
|
||||||
self.update_stats(did, new_delta=cnt)
|
|
||||||
elif type == "rev":
|
|
||||||
self.update_stats(did, review_delta=cnt)
|
|
||||||
elif type == "time":
|
|
||||||
self.update_stats(did, milliseconds_delta=cnt)
|
|
||||||
|
|
||||||
def deckDueTree(self) -> List:
|
|
||||||
"List of (base name, did, rev, lrn, new, children)"
|
|
||||||
print(
|
|
||||||
"deckDueTree() is deprecated; use decks.deck_tree() for a tree without counts, or sched.deck_due_tree()"
|
|
||||||
)
|
|
||||||
return from_json_bytes(self.col._backend.deck_tree_legacy())[5]
|
|
||||||
|
|
||||||
unsuspendCards = unsuspend_cards
|
|
||||||
buryCards = bury_cards
|
|
||||||
suspendCards = suspend_cards
|
|
||||||
forgetCards = schedule_cards_as_new
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import pytest
|
||||||
from anki import hooks
|
from anki import hooks
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from anki.schedv2 import UnburyCurrentDeck
|
from anki.scheduler import UnburyCurrentDeck
|
||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
from tests.shared import getEmptyCol as getEmptyColOrig
|
from tests.shared import getEmptyCol as getEmptyColOrig
|
||||||
|
|
||||||
|
|
|
@ -1056,7 +1056,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
elif isinstance(result, BackendUndo):
|
elif isinstance(result, BackendUndo):
|
||||||
name = result.name
|
name = result.name
|
||||||
|
|
||||||
if reviewing and self.col.sched.is_2021:
|
if reviewing and self.col.sched.version == 3:
|
||||||
# new scheduler will have taken care of updating queue
|
# new scheduler will have taken care of updating queue
|
||||||
just_refresh_reviewer = True
|
just_refresh_reviewer = True
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue