mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -04:00
add daily count updating to backend
This commit is contained in:
parent
fee6cdff22
commit
1fe18718f7
10 changed files with 231 additions and 40 deletions
|
@ -93,6 +93,9 @@ service BackendService {
|
|||
rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut);
|
||||
rpc StudiedToday (StudiedTodayIn) returns (String);
|
||||
rpc CongratsLearnMessage (CongratsLearnMessageIn) returns (String);
|
||||
rpc UpdateStats (UpdateStatsIn) returns (Empty);
|
||||
rpc ExtendLimits (ExtendLimitsIn) returns (Empty);
|
||||
rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut);
|
||||
|
||||
// media
|
||||
|
||||
|
@ -250,8 +253,11 @@ message DeckCommon {
|
|||
uint32 last_day_studied = 3;
|
||||
int32 new_studied = 4;
|
||||
int32 review_studied = 5;
|
||||
int32 milliseconds_studied = 7;
|
||||
|
||||
// previously set in the v1 scheduler,
|
||||
// but not currently used for anything
|
||||
int32 learning_studied = 6;
|
||||
int32 secs_studied = 7;
|
||||
|
||||
bytes other = 255;
|
||||
}
|
||||
|
@ -945,3 +951,21 @@ message RemoveNotesIn {
|
|||
message RemoveCardsIn {
|
||||
repeated int64 card_ids = 1;
|
||||
}
|
||||
|
||||
message UpdateStatsIn {
|
||||
int64 deck_id = 1;
|
||||
int32 new_delta = 2;
|
||||
int32 review_delta = 4;
|
||||
int32 millisecond_delta = 5;
|
||||
}
|
||||
|
||||
message ExtendLimitsIn {
|
||||
int64 deck_id = 1;
|
||||
int32 new_delta = 2;
|
||||
int32 review_delta = 3;
|
||||
}
|
||||
|
||||
message CountsForDeckTodayOut {
|
||||
int32 new = 1;
|
||||
int32 review = 2;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ StockNoteType = pb.StockNoteType
|
|||
SyncAuth = pb.SyncAuth
|
||||
SyncOutput = pb.SyncCollectionOut
|
||||
SyncStatus = pb.SyncStatusOut
|
||||
CountsForDeckToday = pb.CountsForDeckTodayOut
|
||||
|
||||
try:
|
||||
import orjson
|
||||
|
|
|
@ -51,6 +51,10 @@ class Scheduler(V2):
|
|||
# former is for logging new cards, latter also covers filt. decks
|
||||
card.wasNew = card.type == CARD_TYPE_NEW # type: ignore
|
||||
wasNewQ = card.queue == QUEUE_TYPE_NEW
|
||||
|
||||
new_delta = 0
|
||||
review_delta = 0
|
||||
|
||||
if wasNewQ:
|
||||
# came from the new queue, move to learning
|
||||
card.queue = QUEUE_TYPE_LRN
|
||||
|
@ -65,17 +69,22 @@ class Scheduler(V2):
|
|||
# reviews get their ivl boosted on first sight
|
||||
card.ivl = self._dynIvlBoost(card)
|
||||
card.odue = self.today + card.ivl
|
||||
self._updateStats(card, "new")
|
||||
new_delta = +1
|
||||
if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
|
||||
self._answerLrnCard(card, ease)
|
||||
if not wasNewQ:
|
||||
self._updateStats(card, "lrn")
|
||||
elif card.queue == QUEUE_TYPE_REV:
|
||||
self._answerRevCard(card, ease)
|
||||
self._updateStats(card, "rev")
|
||||
review_delta = +1
|
||||
else:
|
||||
raise Exception("Invalid queue '%s'" % card)
|
||||
self._updateStats(card, "time", card.timeTaken())
|
||||
|
||||
self.update_stats(
|
||||
card.did,
|
||||
new_delta=new_delta,
|
||||
review_delta=review_delta,
|
||||
milliseconds_delta=+card.timeTaken(),
|
||||
)
|
||||
|
||||
card.mod = intTime()
|
||||
card.usn = self.col.usn()
|
||||
card.flush()
|
||||
|
@ -447,7 +456,7 @@ and due <= ? limit ?)""",
|
|||
if d["dyn"]:
|
||||
return self.reportLimit
|
||||
c = self.col.decks.confForDid(d["id"])
|
||||
limit = max(0, c["rev"]["perDay"] - self._update_stats(d, "rev", 0))
|
||||
limit = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review)
|
||||
return hooks.scheduler_review_limit_for_single_deck(limit, d)
|
||||
|
||||
def _revForDeck(self, did: int, lim: int) -> int: # type: ignore[override]
|
||||
|
|
|
@ -14,7 +14,12 @@ from anki import hooks
|
|||
from anki.cards import Card
|
||||
from anki.consts import *
|
||||
from anki.lang import _
|
||||
from anki.rsbackend import DeckTreeNode, FormatTimeSpanContext, SchedTimingToday
|
||||
from anki.rsbackend import (
|
||||
CountsForDeckToday,
|
||||
DeckTreeNode,
|
||||
FormatTimeSpanContext,
|
||||
SchedTimingToday,
|
||||
)
|
||||
from anki.utils import ids2str, intTime
|
||||
|
||||
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
|
||||
|
@ -82,7 +87,6 @@ class Scheduler:
|
|||
|
||||
self._answerCard(card, ease)
|
||||
|
||||
self._updateStats(card, "time", card.timeTaken())
|
||||
card.mod = intTime()
|
||||
card.usn = self.col.usn()
|
||||
card.flush()
|
||||
|
@ -94,24 +98,32 @@ class Scheduler:
|
|||
|
||||
card.reps += 1
|
||||
|
||||
new_delta = 0
|
||||
review_delta = 0
|
||||
|
||||
if card.queue == QUEUE_TYPE_NEW:
|
||||
# came from the new queue, move to learning
|
||||
card.queue = QUEUE_TYPE_LRN
|
||||
card.type = CARD_TYPE_LRN
|
||||
# init reps to graduation
|
||||
card.left = self._startingLeft(card)
|
||||
# update daily limit
|
||||
self._updateStats(card, "new")
|
||||
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)
|
||||
# update daily limit
|
||||
self._updateStats(card, "rev")
|
||||
review_delta = +1
|
||||
else:
|
||||
raise Exception("Invalid queue '%s'" % card)
|
||||
|
||||
self.update_stats(
|
||||
card.did,
|
||||
new_delta=new_delta,
|
||||
review_delta=review_delta,
|
||||
milliseconds_delta=+card.timeTaken(),
|
||||
)
|
||||
|
||||
# once a card has been answered once, the original due date
|
||||
# no longer applies
|
||||
if card.odue:
|
||||
|
@ -187,29 +199,33 @@ order by due"""
|
|||
# Rev/lrn/time daily stats
|
||||
##########################################################################
|
||||
|
||||
def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None:
|
||||
for g in [self.col.decks.get(card.did)] + self.col.decks.parents(card.did):
|
||||
self._update_stats(g, type, cnt)
|
||||
self.col.decks.save(g)
|
||||
def update_stats(
|
||||
self, deck_id: int, new_delta=0, review_delta=0, milliseconds_delta=0
|
||||
):
|
||||
self.col.backend.update_stats(
|
||||
deck_id=deck_id,
|
||||
new_delta=new_delta,
|
||||
review_delta=review_delta,
|
||||
millisecond_delta=milliseconds_delta,
|
||||
)
|
||||
|
||||
# resets stat if day has changed, applies delta, and returns modified value
|
||||
def _update_stats(self, deck: Dict, type: str, delta: int) -> int:
|
||||
key = type + "Today"
|
||||
if deck[key][0] != self.today:
|
||||
deck[key] = [self.today, 0]
|
||||
deck[key][1] += delta
|
||||
return deck[key][1]
|
||||
def counts_for_deck_today(self, deck_id: int) -> CountsForDeckToday:
|
||||
return self.col.backend.counts_for_deck_today(deck_id)
|
||||
|
||||
def extendLimits(self, new: int, rev: int) -> None:
|
||||
cur = self.col.decks.current()
|
||||
parents = self.col.decks.parents(cur["id"])
|
||||
children = [
|
||||
self.col.decks.get(did) for did in self.col.decks.child_ids(cur["name"])
|
||||
]
|
||||
for g in [cur] + parents + children:
|
||||
self._update_stats(g, "new", -new)
|
||||
self._update_stats(g, "rev", -rev)
|
||||
self.col.decks.save(g)
|
||||
did = self.col.decks.current()["id"]
|
||||
self.col.backend.extend_limits(deck_id=did, new_delta=new, review_delta=rev)
|
||||
|
||||
# legacy
|
||||
|
||||
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)
|
||||
|
||||
# Deck list
|
||||
##########################################################################
|
||||
|
@ -373,7 +389,7 @@ select count() from
|
|||
if g["dyn"]:
|
||||
return self.dynReportLimit
|
||||
c = self.col.decks.confForDid(g["id"])
|
||||
limit = max(0, c["new"]["perDay"] - self._update_stats(g, "new", 0))
|
||||
limit = max(0, c["new"]["perDay"] - self.counts_for_deck_today(g["id"]).new)
|
||||
return hooks.scheduler_new_limit_for_single_deck(limit, g)
|
||||
|
||||
def totalNewForCurrentDeck(self) -> int:
|
||||
|
@ -766,7 +782,7 @@ and due <= ? limit ?)""",
|
|||
return self.dynReportLimit
|
||||
|
||||
c = self.col.decks.confForDid(d["id"])
|
||||
lim = max(0, c["rev"]["perDay"] - self._update_stats(d, "rev", 0))
|
||||
lim = max(0, c["rev"]["perDay"] - self.counts_for_deck_today(d["id"]).review)
|
||||
|
||||
if parentLimit is not None:
|
||||
lim = min(parentLimit, lim)
|
||||
|
|
|
@ -467,6 +467,40 @@ impl BackendService for Backend {
|
|||
Ok(learning_congrats(input.remaining as usize, input.next_due, &self.i18n).into())
|
||||
}
|
||||
|
||||
fn update_stats(&mut self, input: pb::UpdateStatsIn) -> BackendResult<Empty> {
|
||||
self.with_col(|col| {
|
||||
col.transact(None, |col| {
|
||||
let today = col.current_due_day(0)?;
|
||||
let usn = col.usn()?;
|
||||
col.update_deck_stats(today, usn, input).map(Into::into)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn extend_limits(&mut self, input: pb::ExtendLimitsIn) -> BackendResult<Empty> {
|
||||
self.with_col(|col| {
|
||||
col.transact(None, |col| {
|
||||
let today = col.current_due_day(0)?;
|
||||
let usn = col.usn()?;
|
||||
col.extend_limits(
|
||||
today,
|
||||
usn,
|
||||
input.deck_id.into(),
|
||||
input.new_delta,
|
||||
input.review_delta,
|
||||
)
|
||||
.map(Into::into)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn counts_for_deck_today(
|
||||
&mut self,
|
||||
input: pb::DeckId,
|
||||
) -> BackendResult<pb::CountsForDeckTodayOut> {
|
||||
self.with_col(|col| col.counts_for_deck_today(input.did.into()))
|
||||
}
|
||||
|
||||
// decks
|
||||
//-----------------------------------------------
|
||||
|
||||
|
|
|
@ -69,6 +69,17 @@ impl Deck {
|
|||
kind: DeckKind::Filtered(filt),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_stats_if_day_changed(&mut self, today: u32) {
|
||||
let c = &mut self.common;
|
||||
if c.last_day_studied != today {
|
||||
c.new_studied = 0;
|
||||
c.learning_studied = 0;
|
||||
c.review_studied = 0;
|
||||
c.milliseconds_studied = 0;
|
||||
c.last_day_studied = today;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deck {
|
||||
|
@ -185,7 +196,7 @@ impl From<DeckKind> for pb::deck::Kind {
|
|||
}
|
||||
}
|
||||
|
||||
fn immediate_parent_name(machine_name: &str) -> Option<&str> {
|
||||
pub(crate) fn immediate_parent_name(machine_name: &str) -> Option<&str> {
|
||||
machine_name.rsplitn(2, '\x1f').nth(1)
|
||||
}
|
||||
|
||||
|
@ -465,6 +476,86 @@ impl Collection {
|
|||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Apply input delta to deck, and its parents.
|
||||
/// Caller should ensure transaction.
|
||||
pub(crate) fn update_deck_stats(
|
||||
&mut self,
|
||||
today: u32,
|
||||
usn: Usn,
|
||||
input: pb::UpdateStatsIn,
|
||||
) -> Result<()> {
|
||||
let did = input.deck_id.into();
|
||||
let mutator = |c: &mut DeckCommon| {
|
||||
c.new_studied += input.new_delta;
|
||||
c.review_studied += input.review_delta;
|
||||
c.milliseconds_studied += input.millisecond_delta;
|
||||
};
|
||||
if let Some(mut deck) = self.storage.get_deck(did)? {
|
||||
self.update_deck_stats_single(today, usn, &mut deck, mutator)?;
|
||||
for mut deck in self.storage.parent_decks(&deck)? {
|
||||
self.update_deck_stats_single(today, usn, &mut deck, mutator)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Modify the deck's limits by adjusting the 'done today' count.
|
||||
/// Positive values increase the limit, negative value decrease it.
|
||||
/// Caller should ensure a transaction.
|
||||
pub(crate) fn extend_limits(
|
||||
&mut self,
|
||||
today: u32,
|
||||
usn: Usn,
|
||||
did: DeckID,
|
||||
new_delta: i32,
|
||||
review_delta: i32,
|
||||
) -> Result<()> {
|
||||
let mutator = |c: &mut DeckCommon| {
|
||||
c.new_studied -= new_delta;
|
||||
c.review_studied -= review_delta;
|
||||
};
|
||||
if let Some(mut deck) = self.storage.get_deck(did)? {
|
||||
self.update_deck_stats_single(today, usn, &mut deck, mutator)?;
|
||||
for mut deck in self.storage.parent_decks(&deck)? {
|
||||
self.update_deck_stats_single(today, usn, &mut deck, mutator)?;
|
||||
}
|
||||
for mut deck in self.storage.child_decks(&deck)? {
|
||||
self.update_deck_stats_single(today, usn, &mut deck, mutator)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn counts_for_deck_today(
|
||||
&mut self,
|
||||
did: DeckID,
|
||||
) -> Result<pb::CountsForDeckTodayOut> {
|
||||
let today = self.current_due_day(0)?;
|
||||
let mut deck = self.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?;
|
||||
deck.reset_stats_if_day_changed(today);
|
||||
Ok(pb::CountsForDeckTodayOut {
|
||||
new: deck.common.new_studied,
|
||||
review: deck.common.review_studied,
|
||||
})
|
||||
}
|
||||
|
||||
fn update_deck_stats_single<F>(
|
||||
&mut self,
|
||||
today: u32,
|
||||
usn: Usn,
|
||||
deck: &mut Deck,
|
||||
mutator: F,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnOnce(&mut DeckCommon),
|
||||
{
|
||||
deck.reset_stats_if_day_changed(today);
|
||||
mutator(&mut deck.common);
|
||||
deck.set_modified(usn);
|
||||
self.add_or_update_single_deck(deck, usn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -280,7 +280,7 @@ impl From<&DeckCommonSchema11> for DeckCommon {
|
|||
new_studied: today.new.amount,
|
||||
review_studied: today.rev.amount,
|
||||
learning_studied: today.lrn.amount,
|
||||
secs_studied: common.today.time.amount,
|
||||
milliseconds_studied: common.today.time.amount,
|
||||
other,
|
||||
}
|
||||
}
|
||||
|
@ -393,7 +393,7 @@ impl From<&Deck> for DeckTodaySchema11 {
|
|||
},
|
||||
time: TodayAmountSchema11 {
|
||||
day,
|
||||
amount: c.secs_studied,
|
||||
amount: c.milliseconds_studied,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ use std::{
|
|||
};
|
||||
use unicase::UniCase;
|
||||
|
||||
// fixme: handle mixed case of parents
|
||||
|
||||
fn deck_names_to_tree(names: Vec<(DeckID, String)>) -> DeckTreeNode {
|
||||
let mut top = DeckTreeNode::default();
|
||||
let mut it = names.into_iter().peekable();
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
card::CardID,
|
||||
card::CardQueue,
|
||||
config::SchedulerVersion,
|
||||
decks::immediate_parent_name,
|
||||
decks::{Deck, DeckCommon, DeckID, DeckKindProto, DeckSchema11, DueCounts},
|
||||
err::{AnkiError, DBErrorKind, Result},
|
||||
i18n::{I18n, TR},
|
||||
|
@ -152,6 +153,20 @@ impl SqliteStorage {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn parent_decks(&self, child: &Deck) -> Result<Vec<Deck>> {
|
||||
let mut decks: Vec<Deck> = vec![];
|
||||
while let Some(parent_name) =
|
||||
immediate_parent_name(decks.last().map(|d| &d.name).unwrap_or_else(|| &child.name))
|
||||
{
|
||||
if let Some(parent_did) = self.get_deck_id(parent_name)? {
|
||||
let parent = self.get_deck(parent_did)?.unwrap();
|
||||
decks.push(parent);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(decks)
|
||||
}
|
||||
|
||||
pub(crate) fn due_counts(
|
||||
&self,
|
||||
sched: SchedulerVersion,
|
||||
|
|
|
@ -115,6 +115,9 @@ fn want_release_gil(method: u32) -> bool {
|
|||
BackendMethod::FullDownload => true,
|
||||
BackendMethod::RemoveNotes => true,
|
||||
BackendMethod::RemoveCards => true,
|
||||
BackendMethod::UpdateStats => true,
|
||||
BackendMethod::ExtendLimits => true,
|
||||
BackendMethod::CountsForDeckToday => true,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
Loading…
Reference in a new issue