change get_queued_cards() to no longer return congrats info

This commit is contained in:
Damien Elmes 2021-05-26 12:59:45 +10:00
parent 1d2e89d206
commit 57ec4cc7b5
10 changed files with 92 additions and 150 deletions

View File

@ -2,8 +2,9 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
""" """
This file contains experimental scheduler changes, and is not currently This file contains experimental scheduler changes:
used by Anki.
https://betas.ankiweb.net/2021-scheduler.html
It uses the same DB schema as the V2 scheduler, and 'schedVer' remains It uses the same DB schema as the V2 scheduler, and 'schedVer' remains
as '2' internally. as '2' internally.
@ -11,7 +12,7 @@ as '2' internally.
from __future__ import annotations from __future__ import annotations
from typing import List, Literal, Sequence, Tuple, Union from typing import List, Literal, Sequence, Tuple
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
from anki.cards import Card, CardId from anki.cards import Card, CardId
@ -19,7 +20,6 @@ from anki.collection import OpChanges
from anki.consts import * from anki.consts import *
from anki.decks import DeckId from anki.decks import DeckId
from anki.errors import DBError from anki.errors import DBError
from anki.scheduler.base import CongratsInfo
from anki.scheduler.legacy import SchedulerBaseWithLegacy from anki.scheduler.legacy import SchedulerBaseWithLegacy
from anki.types import assert_exhaustive from anki.types import assert_exhaustive
from anki.utils import intTime from anki.utils import intTime
@ -44,19 +44,11 @@ class Scheduler(SchedulerBaseWithLegacy):
*, *,
fetch_limit: int = 1, fetch_limit: int = 1,
intraday_learning_only: bool = False, intraday_learning_only: bool = False,
) -> Union[QueuedCards, CongratsInfo]: ) -> QueuedCards:
"Returns one or more card ids, or the congratulations screen info." "Returns zero or more pending cards, and the remaining counts. Idempotent."
info = self.col._backend.get_queued_cards( return self.col._backend.get_queued_cards(
fetch_limit=fetch_limit, intraday_learning_only=intraday_learning_only 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 next_states(self, card_id: CardId) -> NextStates: def next_states(self, card_id: CardId) -> NextStates:
"New states corresponding to each answer button press." "New states corresponding to each answer button press."
@ -111,29 +103,23 @@ class Scheduler(SchedulerBaseWithLegacy):
def getCard(self) -> Optional[Card]: def getCard(self) -> Optional[Card]:
"""Fetch the next card from the queue. None if finished.""" """Fetch the next card from the queue. None if finished."""
response = self.get_queued_cards() try:
if isinstance(response, QueuedCards): queued_card = self.get_queued_cards().cards[0]
backend_card = response.cards[0].card except IndexError:
card = Card(self.col)
card._load_from_backend_card(backend_card)
card.startTimer()
return card
else:
return None return None
card = Card(self.col)
card._load_from_backend_card(queued_card.card)
card.startTimer()
return card
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."
info = self.get_queued_cards() return not self.get_queued_cards().cards
return isinstance(info, CongratsInfo)
def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]: def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
info = self.get_queued_cards() info = self.get_queued_cards()
if isinstance(info, CongratsInfo): return (info.new_count, info.learning_count, info.review_count)
counts = [0, 0, 0]
else:
counts = [info.new_count, info.learning_count, info.review_count]
return tuple(counts) # type: ignore
@property @property
def newCount(self) -> int: def newCount(self) -> int:

View File

@ -11,25 +11,13 @@ import re
import unicodedata as ucd import unicodedata as ucd
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from typing import ( from typing import Any, Callable, List, Literal, Match, Optional, Sequence, Tuple, cast
Any,
Callable,
List,
Literal,
Match,
Optional,
Sequence,
Tuple,
Union,
cast,
)
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from anki import hooks from anki import hooks
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount from anki.collection import Config, OpChanges, OpChangesWithCount
from anki.scheduler import CongratsInfo
from anki.scheduler.v3 import CardAnswer, NextStates, QueuedCards from anki.scheduler.v3 import CardAnswer, NextStates, QueuedCards
from anki.scheduler.v3 import Scheduler as V3Scheduler from anki.scheduler.v3 import Scheduler as V3Scheduler
from anki.tags import MARKED_TAG from anki.tags import MARKED_TAG
@ -240,7 +228,7 @@ class Reviewer:
def _get_next_v3_card(self) -> None: def _get_next_v3_card(self) -> None:
assert isinstance(self.mw.col.sched, V3Scheduler) assert isinstance(self.mw.col.sched, V3Scheduler)
output = self.mw.col.sched.get_queued_cards() output = self.mw.col.sched.get_queued_cards()
if isinstance(output, CongratsInfo): if not output.cards:
return return
self._v3 = V3CardInfo.from_queue(output) self._v3 = V3CardInfo.from_queue(output)
self.card = Card(self.mw.col, backend_card=self._v3.top_card().card) self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)

View File

@ -132,7 +132,7 @@ service SchedulingService {
rpc StateIsLeech(SchedulingState) returns (Bool); rpc StateIsLeech(SchedulingState) returns (Bool);
rpc AnswerCard(CardAnswer) returns (OpChanges); rpc AnswerCard(CardAnswer) returns (OpChanges);
rpc UpgradeScheduler(Empty) returns (Empty); rpc UpgradeScheduler(Empty) returns (Empty);
rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut); rpc GetQueuedCards(GetQueuedCardsIn) returns (QueuedCards);
} }
service DecksService { service DecksService {
@ -1563,13 +1563,6 @@ message QueuedCards {
uint32 review_count = 4; uint32 review_count = 4;
} }
message GetQueuedCardsOut {
oneof value {
QueuedCards queued_cards = 1;
CongratsInfoOut congrats_info = 2;
}
}
message OpChanges { message OpChanges {
bool card = 1; bool card = 1;
bool note = 2; bool note = 2;

View File

@ -176,9 +176,10 @@ impl SchedulingService for Backend {
.map(Into::into) .map(Into::into)
} }
fn get_queued_cards(&self, input: pb::GetQueuedCardsIn) -> Result<pb::GetQueuedCardsOut> { fn get_queued_cards(&self, input: pb::GetQueuedCardsIn) -> Result<pb::QueuedCards> {
self.with_col(|col| { self.with_col(|col| {
col.get_queued_cards(input.fetch_limit as usize, input.intraday_learning_only) col.get_queued_cards(input.fetch_limit as usize, input.intraday_learning_only)
.map(Into::into)
}) })
} }
} }

View File

@ -693,7 +693,7 @@ mod test {
col.storage.db_scalar::<u32>("select count() from graves")?, col.storage.db_scalar::<u32>("select count() from graves")?,
0 0
); );
assert_eq!(col.next_card()?.is_some(), false); assert_eq!(col.get_next_card()?.is_some(), false);
Ok(()) Ok(())
}; };
@ -704,7 +704,7 @@ mod test {
col.storage.db_scalar::<u32>("select count() from graves")?, col.storage.db_scalar::<u32>("select count() from graves")?,
0 0
); );
assert_eq!(col.next_card()?.is_some(), true); assert_eq!(col.get_next_card()?.is_some(), true);
Ok(()) Ok(())
}; };
@ -732,7 +732,7 @@ mod test {
col.storage.db_scalar::<u32>("select count() from graves")?, col.storage.db_scalar::<u32>("select count() from graves")?,
3 3
); );
assert_eq!(col.next_card()?.is_some(), false); assert_eq!(col.get_next_card()?.is_some(), false);
Ok(()) Ok(())
}; };

View File

@ -401,7 +401,7 @@ pub mod test_helpers {
where where
F: FnOnce(&NextCardStates) -> CardState, F: FnOnce(&NextCardStates) -> CardState,
{ {
let queued = self.next_card()?.unwrap(); let queued = self.get_next_card()?.unwrap();
let new_state = get_state(&queued.next_states); let new_state = get_state(&queued.next_states);
self.answer_card(&CardAnswer { self.answer_card(&CardAnswer {
card_id: queued.card.id, card_id: queued.card.id,

View File

@ -37,7 +37,7 @@ impl QueueEntry {
} }
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum QueueEntryKind { pub enum QueueEntryKind {
New, New,
Learning, Learning,
Review, Review,

View File

@ -16,7 +16,7 @@ pub(crate) use main::{MainQueueEntry, MainQueueEntryKind};
use self::undo::QueueUpdate; use self::undo::QueueUpdate;
use super::{states::NextCardStates, timing::SchedTimingToday}; use super::{states::NextCardStates, timing::SchedTimingToday};
use crate::{backend_proto as pb, prelude::*, timestamp::TimestampSecs}; use crate::{prelude::*, timestamp::TimestampSecs};
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct CardQueues { pub(crate) struct CardQueues {
@ -32,26 +32,81 @@ pub(crate) struct CardQueues {
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub(crate) struct Counts { pub struct Counts {
pub new: usize, pub new: usize,
pub learning: usize, pub learning: usize,
pub review: usize, pub review: usize,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub(crate) struct QueuedCard { pub struct QueuedCard {
pub card: Card, pub card: Card,
pub kind: QueueEntryKind, pub kind: QueueEntryKind,
pub next_states: NextCardStates, pub next_states: NextCardStates,
} }
pub(crate) struct QueuedCards { #[derive(Debug)]
pub struct QueuedCards {
pub cards: Vec<QueuedCard>, pub cards: Vec<QueuedCard>,
pub new_count: usize, pub new_count: usize,
pub learning_count: usize, pub learning_count: usize,
pub review_count: usize, pub review_count: usize,
} }
impl Collection {
pub fn get_next_card(&mut self) -> Result<Option<QueuedCard>> {
self.get_queued_cards(1, false)
.map(|queued| queued.cards.get(0).cloned())
}
pub fn get_queued_cards(
&mut self,
fetch_limit: usize,
intraday_learning_only: bool,
) -> Result<QueuedCards> {
let queues = self.get_queues()?;
let counts = queues.counts();
let entries: Vec<_> = if intraday_learning_only {
queues
.intraday_now_iter()
.chain(queues.intraday_ahead_iter())
.map(Into::into)
.collect()
} else {
queues.iter().take(fetch_limit).collect()
};
let cards: Vec<_> = entries
.into_iter()
.map(|entry| {
let card = self
.storage
.get_card(entry.card_id())?
.ok_or(AnkiError::NotFound)?;
if card.mtime != entry.mtime() {
return Err(AnkiError::invalid_input(
"bug: card modified without updating queue",
));
}
// fixme: pass in card instead of id
let next_states = self.get_next_card_states(card.id)?;
Ok(QueuedCard {
card,
next_states,
kind: entry.kind(),
})
})
.collect::<Result<_>>()?;
Ok(QueuedCards {
cards,
new_count: counts.new,
learning_count: counts.learning,
review_count: counts.review,
})
}
}
impl CardQueues { impl CardQueues {
/// An iterator over the card queues, in the order the cards will /// An iterator over the card queues, in the order the cards will
/// be presented. /// be presented.
@ -103,26 +158,6 @@ impl CardQueues {
} }
impl Collection { impl Collection {
pub(crate) fn get_queued_cards(
&mut self,
fetch_limit: usize,
intraday_learning_only: bool,
) -> Result<pb::GetQueuedCardsOut> {
if let Some(next_cards) = self.next_cards(fetch_limit, intraday_learning_only)? {
Ok(pb::GetQueuedCardsOut {
value: Some(pb::get_queued_cards_out::Value::QueuedCards(
next_cards.into(),
)),
})
} else {
Ok(pb::GetQueuedCardsOut {
value: Some(pb::get_queued_cards_out::Value::CongratsInfo(
self.congrats_info()?,
)),
})
}
}
/// This is automatically done when transact() is called for everything /// This is automatically done when transact() is called for everything
/// except card answers, so unless you are modifying state outside of a /// except card answers, so unless you are modifying state outside of a
/// transaction, you probably don't need this. /// transaction, you probably don't need this.
@ -177,74 +212,13 @@ impl Collection {
Ok(self.state.card_queues.as_mut().unwrap()) Ok(self.state.card_queues.as_mut().unwrap())
} }
fn next_cards(
&mut self,
fetch_limit: usize,
intraday_learning_only: bool,
) -> Result<Option<QueuedCards>> {
let queues = self.get_queues()?;
let counts = queues.counts();
let entries: Vec<_> = if intraday_learning_only {
queues
.intraday_now_iter()
.chain(queues.intraday_ahead_iter())
.map(Into::into)
.collect()
} else {
queues.iter().take(fetch_limit).collect()
};
if entries.is_empty() {
Ok(None)
} else {
let cards: Vec<_> = entries
.into_iter()
.map(|entry| {
let card = self
.storage
.get_card(entry.card_id())?
.ok_or(AnkiError::NotFound)?;
if card.mtime != entry.mtime() {
return Err(AnkiError::invalid_input(
"bug: card modified without updating queue",
));
}
// fixme: pass in card instead of id
let next_states = self.get_next_card_states(card.id)?;
Ok(QueuedCard {
card,
next_states,
kind: entry.kind(),
})
})
.collect::<Result<_>>()?;
Ok(Some(QueuedCards {
cards,
new_count: counts.new,
learning_count: counts.learning,
review_count: counts.review,
}))
}
}
} }
// test helpers // test helpers
#[cfg(test)] #[cfg(test)]
impl Collection { impl Collection {
pub(crate) fn next_card(&mut self) -> Result<Option<QueuedCard>> {
Ok(self
.next_cards(1, false)?
.map(|mut resp| resp.cards.pop().unwrap()))
}
fn get_queue_single(&mut self) -> Result<QueuedCards> {
self.next_cards(1, false)?.ok_or(AnkiError::NotFound)
}
pub(crate) fn counts(&mut self) -> [usize; 3] { pub(crate) fn counts(&mut self) -> [usize; 3] {
self.get_queue_single() self.get_queued_cards(1, false)
.map(|q| [q.new_count, q.learning_count, q.review_count]) .map(|q| [q.new_count, q.learning_count, q.review_count])
.unwrap_or([0; 3]) .unwrap_or([0; 3])
} }

View File

@ -102,7 +102,7 @@ mod test {
col.storage.update_deck_conf(&conf)?; col.storage.update_deck_conf(&conf)?;
// get the first card // get the first card
let queued = col.next_card()?.unwrap(); let queued = col.get_next_card()?.unwrap();
let cid = queued.card.id; let cid = queued.card.id;
let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1]; let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1];
@ -152,7 +152,7 @@ mod test {
let deck = col.get_deck(DeckId(1))?.unwrap(); let deck = col.get_deck(DeckId(1))?.unwrap();
assert_eq!(deck.common.review_studied, 1); assert_eq!(deck.common.review_studied, 1);
assert_eq!(col.next_card()?.is_some(), false); assert_eq!(col.get_next_card()?.is_some(), false);
Ok(()) Ok(())
}; };
@ -177,7 +177,7 @@ mod test {
let deck = col.get_deck(DeckId(1))?.unwrap(); let deck = col.get_deck(DeckId(1))?.unwrap();
assert_eq!(deck.common.review_studied, 0); assert_eq!(deck.common.review_studied, 0);
assert_eq!(col.next_card()?.is_some(), true); assert_eq!(col.get_next_card()?.is_some(), true);
assert_eq!(col.counts(), [0, 0, 1]); assert_eq!(col.counts(), [0, 0, 1]);
Ok(()) Ok(())

View File

@ -135,7 +135,7 @@ fn fuzz_range(interval: f32, factor: f32, minimum: f32) -> (f32, f32) {
(interval - delta, interval + delta + 1.0) (interval - delta, interval + delta + 1.0)
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct NextCardStates { pub struct NextCardStates {
pub current: CardState, pub current: CardState,
pub again: CardState, pub again: CardState,