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
"""
This file contains experimental scheduler changes, and is not currently
used by Anki.
This file contains experimental scheduler changes:
https://betas.ankiweb.net/2021-scheduler.html
It uses the same DB schema as the V2 scheduler, and 'schedVer' remains
as '2' internally.
@ -11,7 +12,7 @@ as '2' internally.
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
from anki.cards import Card, CardId
@ -19,7 +20,6 @@ from anki.collection import OpChanges
from anki.consts import *
from anki.decks import DeckId
from anki.errors import DBError
from anki.scheduler.base import CongratsInfo
from anki.scheduler.legacy import SchedulerBaseWithLegacy
from anki.types import assert_exhaustive
from anki.utils import intTime
@ -44,19 +44,11 @@ class Scheduler(SchedulerBaseWithLegacy):
*,
fetch_limit: int = 1,
intraday_learning_only: bool = False,
) -> Union[QueuedCards, CongratsInfo]:
"Returns one or more card ids, or the congratulations screen info."
info = self.col._backend.get_queued_cards(
) -> QueuedCards:
"Returns zero or more pending cards, and the remaining counts. Idempotent."
return 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 next_states(self, card_id: CardId) -> NextStates:
"New states corresponding to each answer button press."
@ -111,29 +103,23 @@ class Scheduler(SchedulerBaseWithLegacy):
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:
try:
queued_card = self.get_queued_cards().cards[0]
except IndexError:
return None
card = Card(self.col)
card._load_from_backend_card(queued_card.card)
card.startTimer()
return card
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)
return not self.get_queued_cards().cards
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
return (info.new_count, info.learning_count, info.review_count)
@property
def newCount(self) -> int:

View File

@ -11,25 +11,13 @@ import re
import unicodedata as ucd
from dataclasses import dataclass
from enum import Enum, auto
from typing import (
Any,
Callable,
List,
Literal,
Match,
Optional,
Sequence,
Tuple,
Union,
cast,
)
from typing import Any, Callable, List, Literal, Match, Optional, Sequence, Tuple, cast
from PyQt5.QtCore import Qt
from anki import hooks
from anki.cards import Card, CardId
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 Scheduler as V3Scheduler
from anki.tags import MARKED_TAG
@ -240,7 +228,7 @@ class Reviewer:
def _get_next_v3_card(self) -> None:
assert isinstance(self.mw.col.sched, V3Scheduler)
output = self.mw.col.sched.get_queued_cards()
if isinstance(output, CongratsInfo):
if not output.cards:
return
self._v3 = V3CardInfo.from_queue(output)
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 AnswerCard(CardAnswer) returns (OpChanges);
rpc UpgradeScheduler(Empty) returns (Empty);
rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut);
rpc GetQueuedCards(GetQueuedCardsIn) returns (QueuedCards);
}
service DecksService {
@ -1563,13 +1563,6 @@ message QueuedCards {
uint32 review_count = 4;
}
message GetQueuedCardsOut {
oneof value {
QueuedCards queued_cards = 1;
CongratsInfoOut congrats_info = 2;
}
}
message OpChanges {
bool card = 1;
bool note = 2;

View File

@ -176,9 +176,10 @@ impl SchedulingService for Backend {
.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| {
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")?,
0
);
assert_eq!(col.next_card()?.is_some(), false);
assert_eq!(col.get_next_card()?.is_some(), false);
Ok(())
};
@ -704,7 +704,7 @@ mod test {
col.storage.db_scalar::<u32>("select count() from graves")?,
0
);
assert_eq!(col.next_card()?.is_some(), true);
assert_eq!(col.get_next_card()?.is_some(), true);
Ok(())
};
@ -732,7 +732,7 @@ mod test {
col.storage.db_scalar::<u32>("select count() from graves")?,
3
);
assert_eq!(col.next_card()?.is_some(), false);
assert_eq!(col.get_next_card()?.is_some(), false);
Ok(())
};

View File

@ -401,7 +401,7 @@ pub mod test_helpers {
where
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);
self.answer_card(&CardAnswer {
card_id: queued.card.id,

View File

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

View File

@ -16,7 +16,7 @@ pub(crate) use main::{MainQueueEntry, MainQueueEntryKind};
use self::undo::QueueUpdate;
use super::{states::NextCardStates, timing::SchedTimingToday};
use crate::{backend_proto as pb, prelude::*, timestamp::TimestampSecs};
use crate::{prelude::*, timestamp::TimestampSecs};
#[derive(Debug)]
pub(crate) struct CardQueues {
@ -32,26 +32,81 @@ pub(crate) struct CardQueues {
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct Counts {
pub struct Counts {
pub new: usize,
pub learning: usize,
pub review: usize,
}
#[derive(Debug)]
pub(crate) struct QueuedCard {
#[derive(Debug, Clone)]
pub struct QueuedCard {
pub card: Card,
pub kind: QueueEntryKind,
pub next_states: NextCardStates,
}
pub(crate) struct QueuedCards {
#[derive(Debug)]
pub struct QueuedCards {
pub cards: Vec<QueuedCard>,
pub new_count: usize,
pub learning_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 {
/// An iterator over the card queues, in the order the cards will
/// be presented.
@ -103,26 +158,6 @@ impl CardQueues {
}
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
/// except card answers, so unless you are modifying state outside of a
/// transaction, you probably don't need this.
@ -177,74 +212,13 @@ impl Collection {
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
#[cfg(test)]
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] {
self.get_queue_single()
self.get_queued_cards(1, false)
.map(|q| [q.new_count, q.learning_count, q.review_count])
.unwrap_or([0; 3])
}

View File

@ -102,7 +102,7 @@ mod test {
col.storage.update_deck_conf(&conf)?;
// get the first card
let queued = col.next_card()?.unwrap();
let queued = col.get_next_card()?.unwrap();
let cid = queued.card.id;
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();
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(())
};
@ -177,7 +177,7 @@ mod test {
let deck = col.get_deck(DeckId(1))?.unwrap();
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]);
Ok(())

View File

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