plug new answering code in

This is not the way the code is intended to be used, but making it
conform to the existing API allows us to exercise the existing unit
tests and provides partial backwards compatibility.

- Leech handling is currently broken
- Fix answered_at in wrong units, and not being used
This commit is contained in:
Damien Elmes 2021-02-22 12:29:31 +10:00
parent f811beae5e
commit 3af5221895
5 changed files with 81 additions and 84 deletions

View file

@ -5,6 +5,7 @@ persistent = no
[TYPECHECK] [TYPECHECK]
ignored-classes= ignored-classes=
FormatTimespanIn, FormatTimespanIn,
AnswerCardIn,
UnburyCardsInCurrentDeckIn, UnburyCardsInCurrentDeckIn,
BuryOrSuspendCardsIn BuryOrSuspendCardsIn

View file

@ -15,8 +15,8 @@ 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, DeckManager, DeckTreeNode, QueueConfig from anki.decks import Deck, DeckConfig, DeckManager, DeckTreeNode, QueueConfig
from anki.lang import FormatTimeSpan
from anki.notes import Note from anki.notes import Note
from anki.types import assert_exhaustive
from anki.utils import from_json_bytes, ids2str, intTime from anki.utils import from_json_bytes, ids2str, intTime
CongratsInfo = _pb.CongratsInfoOut CongratsInfo = _pb.CongratsInfoOut
@ -476,42 +476,33 @@ limit ?"""
card.flush() card.flush()
def _answerCard(self, card: Card, ease: int) -> None: def _answerCard(self, card: Card, ease: int) -> None:
if self._previewingCard(card): states = self.col._backend.get_next_card_states(card.id)
self._answerCardPreview(card, ease) if ease == BUTTON_ONE:
return new_state = states.again
rating = _pb.AnswerCardIn.AGAIN
card.reps += 1 elif ease == BUTTON_TWO:
new_state = states.hard
new_delta = 0 rating = _pb.AnswerCardIn.HARD
review_delta = 0 elif ease == BUTTON_THREE:
new_state = states.good
if card.queue == QUEUE_TYPE_NEW: rating = _pb.AnswerCardIn.GOOD
# came from the new queue, move to learning elif ease == BUTTON_FOUR:
card.queue = QUEUE_TYPE_LRN new_state = states.easy
card.type = CARD_TYPE_LRN rating = _pb.AnswerCardIn.EASY
# init reps to graduation
card.left = self._startingLeft(card)
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: else:
raise Exception(f"Invalid queue '{card}'") assert False, "invalid ease"
self.update_stats( self.col._backend.answer_card(
card.did, card_id=card.id,
new_delta=new_delta, current_state=states.current,
review_delta=review_delta, new_state=new_state,
milliseconds_delta=+card.timeTaken(), rating=rating,
answered_at_millis=intTime(1000),
milliseconds_taken=card.timeTaken(),
) )
# once a card has been answered once, the original due date # fixme: tests assume card will be mutated, so we need to reload it
# no longer applies card.load()
if card.odue:
card.odue = 0
def _maybe_requeue_card(self, card: Card) -> None: def _maybe_requeue_card(self, card: Card) -> None:
# preview cards # preview cards
@ -1053,51 +1044,59 @@ limit ?"""
# Next times # 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: def nextIvl(self, card: Card, ease: int) -> Any:
"Return the next interval for CARD, in seconds." "Don't use this - it is only required by tests, and will be moved in the future."
# preview mode? states = self.col._backend.get_next_card_states(card.id)
if self._previewingCard(card):
if ease == BUTTON_ONE:
return self._previewDelay(card)
return 0
# (re)learning?
if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
return self._nextLrnIvl(card, ease)
elif ease == BUTTON_ONE:
# lapse
conf = self._lapseConf(card)
if conf["delays"]:
return conf["delays"][0] * 60
return self._lapseIvl(card, conf) * 86400
else:
# review
early = card.odid and (card.odue > self.today)
if early:
return self._earlyReviewIvl(card, ease) * 86400
else:
return self._nextRevIvl(card, ease, fuzz=False) * 86400
# this isn't easily extracted from the learn code
def _nextLrnIvl(self, card: Card, ease: int) -> Any:
if card.queue == QUEUE_TYPE_NEW:
card.left = self._startingLeft(card)
conf = self._lrnConf(card)
if ease == BUTTON_ONE: if ease == BUTTON_ONE:
# fail new_state = states.again
return self._delayForGrade(conf, len(conf["delays"]))
elif ease == BUTTON_TWO: elif ease == BUTTON_TWO:
return self._delayForRepeatingGrade(conf, card.left) new_state = states.hard
elif ease == BUTTON_THREE:
new_state = states.good
elif ease == BUTTON_FOUR: elif ease == BUTTON_FOUR:
return self._graduatingIvl(card, conf, True, fuzz=False) * 86400 new_state = states.easy
else: # ease == BUTTON_THREE else:
left = card.left % 1000 - 1 assert False, "invalid ease"
if left <= 0:
# graduate return self._interval_for_state(new_state)
return self._graduatingIvl(card, conf, False, fuzz=False) * 86400
else:
return self._delayForGrade(conf, left)
# Leeches # Leeches
########################################################################## ##########################################################################
@ -1181,13 +1180,8 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str: def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:
"Return the next interval for CARD as a string." "Return the next interval for CARD as a string."
ivl_secs = self.nextIvl(card, ease) states = self.col._backend.get_next_card_states(card.id)
if not ivl_secs: return self.col._backend.describe_next_states(states)[ease - 1]
return self.col.tr(TR.SCHEDULING_END)
s = self.col.format_timespan(ivl_secs, FormatTimeSpan.ANSWER_BUTTONS)
if ivl_secs < self.col.conf["collapseTime"]:
s = "<" + s
return s
# Deck list # Deck list
########################################################################## ##########################################################################

View file

@ -1336,6 +1336,6 @@ message AnswerCardIn {
SchedulingState current_state = 2; SchedulingState current_state = 2;
SchedulingState new_state = 3; SchedulingState new_state = 3;
Rating rating = 4; Rating rating = 4;
int64 answered_at = 5; int64 answered_at_millis = 5;
uint32 milliseconds_taken = 6; uint32 milliseconds_taken = 6;
} }

View file

@ -14,7 +14,7 @@ impl From<pb::AnswerCardIn> for CardAnswer {
rating: answer.rating().into(), rating: answer.rating().into(),
current_state: answer.current_state.unwrap_or_default().into(), current_state: answer.current_state.unwrap_or_default().into(),
new_state: answer.new_state.unwrap_or_default().into(), new_state: answer.new_state.unwrap_or_default().into(),
answered_at: TimestampSecs(answer.answered_at), answered_at: TimestampMillis(answer.answered_at_millis),
milliseconds_taken: answer.milliseconds_taken, milliseconds_taken: answer.milliseconds_taken,
} }
} }

View file

@ -32,7 +32,7 @@ pub struct CardAnswer {
pub current_state: CardState, pub current_state: CardState,
pub new_state: CardState, pub new_state: CardState,
pub rating: Rating, pub rating: Rating,
pub answered_at: TimestampSecs, pub answered_at: TimestampMillis,
pub milliseconds_taken: u32, pub milliseconds_taken: u32,
} }
@ -404,10 +404,11 @@ impl RevlogEntryPartial {
usn: Usn, usn: Usn,
cid: CardID, cid: CardID,
button_chosen: u8, button_chosen: u8,
answered_at: TimestampMillis,
taken_millis: u32, taken_millis: u32,
) -> RevlogEntry { ) -> RevlogEntry {
RevlogEntry { RevlogEntry {
id: TimestampMillis::now(), id: answered_at,
cid, cid,
usn, usn,
button_chosen, button_chosen,
@ -450,6 +451,7 @@ impl Collection {
usn, usn,
answer.card_id, answer.card_id,
button_chosen, button_chosen,
answer.answered_at,
answer.milliseconds_taken, answer.milliseconds_taken,
); );
self.storage.add_revlog_entry(&revlog)?; self.storage.add_revlog_entry(&revlog)?;