move unbury/unsuspend routines into backend

This commit is contained in:
Damien Elmes 2020-08-29 22:02:22 +10:00
parent e80b3eeeef
commit ccfa989c62
14 changed files with 272 additions and 200 deletions

View file

@ -56,6 +56,10 @@ message CardID {
int64 cid = 1;
}
message CardIDs {
repeated int64 cids = 1;
}
message DeckID {
int64 did = 1;
}
@ -96,6 +100,8 @@ service BackendService {
rpc ExtendLimits (ExtendLimitsIn) returns (Empty);
rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut);
rpc CongratsInfo (Empty) returns (CongratsInfoOut);
rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty);
rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty);
// stats
@ -1027,3 +1033,12 @@ message CongratsInfoOut {
bool is_filtered_deck = 7;
bool bridge_commands_supported = 8;
}
message UnburyCardsInCurrentDeckIn {
enum Mode {
ALL = 0;
SCHED_ONLY = 1;
USER_ONLY = 2;
}
Mode mode = 1;
}

View file

@ -121,32 +121,6 @@ class Scheduler(V2):
else:
return 3
def unburyCards(self) -> None:
"Unbury cards."
self.col.log(
self.col.db.list(
f"select id from cards where queue = {QUEUE_TYPE_SIBLING_BURIED}"
)
)
self.col.db.execute(
f"update cards set queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED}"
)
def unburyCardsForDeck(self) -> None: # type: ignore[override]
sids = self._deckLimit()
self.col.log(
self.col.db.list(
f"select id from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s"
% sids
)
)
self.col.db.execute(
f"update cards set mod=?,usn=?,queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s"
% sids,
intTime(),
self.col.usn(),
)
# Getting the next card
##########################################################################
@ -848,16 +822,6 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
self.col.usn(),
)
def unsuspendCards(self, ids: List[int]) -> None:
"Unsuspend cards."
self.col.log(ids)
self.col.db.execute(
"update cards set queue=type,mod=?,usn=? "
f"where queue = {QUEUE_TYPE_SUSPENDED} and id in " + ids2str(ids),
intTime(),
self.col.usn(),
)
def buryCards(self, cids: List[int], manual: bool = False) -> None:
# v1 only supported automatic burying
assert not manual

View file

@ -7,7 +7,18 @@ import pprint
import random
import time
from heapq import *
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)
import anki # pylint: disable=unused-import
import anki.backend_pb2 as pb
@ -25,6 +36,12 @@ from anki.rsbackend import (
)
from anki.utils import ids2str, intTime
UnburyCurrentDeckMode = pb.UnburyCardsInCurrentDeckIn.Mode # pylint: disable=no-member
if TYPE_CHECKING:
UnburyCurrentDeckModeValue = (
pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint: disable=no-member
)
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
# 4=preview, -1=suspended, -2=sibling buried, -3=manually buried
@ -1265,12 +1282,6 @@ where id = ?
self.today = timing.days_elapsed
self.dayCutoff = timing.next_day_at
# unbury if the day has rolled over
unburied = self.col.conf.get("lastUnburied", 0)
if unburied < self.today or self.today + 7 < unburied:
self.unburyCards()
self.col.conf["lastUnburied"] = self.today
def _checkDay(self) -> None:
# check if the day has rolled over
if time.time() > self.dayCutoff:
@ -1364,16 +1375,16 @@ where id = ?
# Suspending & burying
##########################################################################
# learning and relearning cards may be seconds-based or day-based;
# other types map directly to queues
_restoreQueueSnippet = f"""
queue = (case when type in ({CARD_TYPE_LRN},{CARD_TYPE_RELEARNING}) then
(case when (case when odue then odue else due end) > 1000000000 then
{QUEUE_TYPE_LRN} else {QUEUE_TYPE_DAY_LEARN_RELEARN} end)
else
type
end)
"""
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: UnburyCurrentDeckModeValue = UnburyCurrentDeckMode.ALL,
) -> None:
self.col.backend.unbury_cards_in_current_deck(mode)
def suspendCards(self, ids: List[int]) -> None:
"Suspend cards."
@ -1385,18 +1396,6 @@ end)
self.col.usn(),
)
def unsuspendCards(self, ids: List[int]) -> None:
"Unsuspend cards."
self.col.log(ids)
self.col.db.execute(
(
f"update cards set %s,mod=?,usn=? where queue = {QUEUE_TYPE_SUSPENDED} and id in %s"
)
% (self._restoreQueueSnippet, ids2str(ids)),
intTime(),
self.col.usn(),
)
def buryCards(self, cids: List[int], manual: bool = True) -> None:
queue = manual and QUEUE_TYPE_MANUALLY_BURIED or QUEUE_TYPE_SIBLING_BURIED
self.col.log(cids)
@ -1416,42 +1415,24 @@ update cards set queue=?,mod=?,usn=? where id in """
)
self.buryCards(cids)
# legacy
def unburyCards(self) -> None:
"Unbury all buried cards in all decks."
self.col.log(
self.col.db.list(
f"select id from cards where queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})"
)
)
self.col.db.execute(
f"update cards set %s where queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})"
% self._restoreQueueSnippet
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:
if type == "all":
queue = (
f"queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})"
)
mode = UnburyCurrentDeckMode.ALL
elif type == "manual":
queue = f"queue = {QUEUE_TYPE_MANUALLY_BURIED}"
elif type == "siblings":
queue = f"queue = {QUEUE_TYPE_SIBLING_BURIED}"
else:
raise Exception("unknown type")
mode = UnburyCurrentDeckMode.USER_ONLY
else: # elif type == "siblings":
mode = UnburyCurrentDeckMode.SCHED_ONLY
self.unbury_cards_in_current_deck(mode)
self.col.log(
self.col.db.list(
"select id from cards where %s and did in %s"
% (queue, self._deckLimit())
)
)
self.col.db.execute(
"update cards set mod=?,usn=?,%s where %s and did in %s"
% (self._restoreQueueSnippet, queue, self._deckLimit()),
intTime(),
self.col.usn(),
)
unsuspendCards = unsuspend_cards
# Sibling spacing
##########################################################################

View file

@ -503,7 +503,7 @@ def test_misc():
col.sched.buryNote(c.nid)
col.reset()
assert not col.sched.getCard()
col.sched.unburyCards()
col.sched.unbury_cards_in_current_deck()
col.reset()
assert col.sched.getCard()

View file

@ -6,6 +6,7 @@ import time
from anki import hooks
from anki.consts import *
from anki.lang import without_unicode_isolation
from anki.schedv2 import UnburyCurrentDeckMode
from anki.utils import intTime
from tests.shared import getEmptyCol as getEmptyColOrig
@ -609,22 +610,18 @@ def test_bury():
col.reset()
assert not col.sched.getCard()
col.sched.unburyCardsForDeck( # pylint: disable=unexpected-keyword-arg
type="manual"
)
col.sched.unbury_cards_in_current_deck(UnburyCurrentDeckMode.USER_ONLY)
c.load()
assert c.queue == QUEUE_TYPE_NEW
c2.load()
assert c2.queue == QUEUE_TYPE_SIBLING_BURIED
col.sched.unburyCardsForDeck( # pylint: disable=unexpected-keyword-arg
type="siblings"
)
col.sched.unbury_cards_in_current_deck(UnburyCurrentDeckMode.SCHED_ONLY)
c2.load()
assert c2.queue == QUEUE_TYPE_NEW
col.sched.buryCards([c.id, c2.id])
col.sched.unburyCardsForDeck(type="all") # pylint: disable=unexpected-keyword-arg
col.sched.unbury_cards_in_current_deck()
col.reset()
@ -1214,7 +1211,7 @@ def test_moveVersions():
assert c.queue == QUEUE_TYPE_SIBLING_BURIED
# and it should be new again when unburied
col.sched.unburyCards()
col.sched.unbury_cards_in_current_deck()
c.load()
assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW

View file

@ -235,6 +235,12 @@ impl From<pb::CardId> for CardID {
}
}
impl pb::CardIDs {
fn into_native(self) -> Vec<CardID> {
self.cids.into_iter().map(CardID).collect()
}
}
impl From<pb::NoteId> for NoteID {
fn from(nid: pb::NoteId) -> Self {
NoteID(nid.nid)
@ -442,8 +448,14 @@ impl BackendService for Backend {
// scheduling
//-----------------------------------------------
/// This behaves like _updateCutoff() in older code - it also unburies at the start of
/// a new day.
fn sched_timing_today(&mut self, _input: pb::Empty) -> Result<pb::SchedTimingTodayOut> {
self.with_col(|col| col.timing_today().map(Into::into))
self.with_col(|col| {
let timing = col.timing_today()?;
col.unbury_if_day_rolled_over(timing)?;
Ok(timing.into())
})
}
fn local_minutes_west(&mut self, input: pb::Int64) -> BackendResult<pb::Int32> {
@ -502,6 +514,23 @@ impl BackendService for Backend {
self.with_col(|col| col.congrats_info())
}
fn restore_buried_and_suspended_cards(&mut self, input: pb::CardIDs) -> BackendResult<Empty> {
self.with_col(|col| {
col.unbury_or_unsuspend_cards(&input.into_native())
.map(Into::into)
})
}
fn unbury_cards_in_current_deck(
&mut self,
input: pb::UnburyCardsInCurrentDeckIn,
) -> BackendResult<Empty> {
self.with_col(|col| {
col.unbury_cards_in_current_deck(input.mode())
.map(Into::into)
})
}
// statistics
//-----------------------------------------------
@ -695,7 +724,7 @@ impl BackendService for Backend {
.storage
.get_card(card.id)?
.ok_or_else(|| AnkiError::invalid_input("missing card"))?;
ctx.update_card(&mut card, &orig)
ctx.update_card(&mut card, &orig, ctx.usn()?)
})
})
.map(Into::into)

View file

@ -97,6 +97,11 @@ impl Default for Card {
}
impl Card {
pub fn set_modified(&mut self, usn: Usn) {
self.mtime = TimestampSecs::now();
self.usn = usn;
}
pub(crate) fn return_home(&mut self, sched: SchedulerVersion) {
if self.odid.0 == 0 {
// this should not happen
@ -149,34 +154,17 @@ impl Card {
self.ctype = CardType::New;
}
}
pub(crate) fn restore_queue_after_bury_or_suspend(&mut self) {
self.queue = match self.ctype {
CardType::Learn | CardType::Relearn => {
let original_due = if self.odue > 0 { self.odue } else { self.due };
if original_due > 1_000_000_000 {
// previous interval was in seconds
CardQueue::Learn
} else {
// previous interval was in days
CardQueue::DayLearn
}
}
CardType::New => CardQueue::New,
CardType::Review => CardQueue::Review,
}
}
}
#[derive(Debug)]
pub(crate) struct UpdateCardUndo(Card);
impl Undoable for UpdateCardUndo {
fn apply(&self, col: &mut crate::collection::Collection) -> Result<()> {
fn apply(&self, col: &mut crate::collection::Collection, usn: Usn) -> Result<()> {
let current = col
.storage
.get_card(self.0.id)?
.ok_or_else(|| AnkiError::invalid_input("card disappeared"))?;
col.update_card(&mut self.0.clone(), &current)
col.update_card(&mut self.0.clone(), &current, usn)
}
}
@ -202,19 +190,18 @@ impl Collection {
.ok_or_else(|| AnkiError::invalid_input("no such card"))?;
let mut card = orig.clone();
func(&mut card)?;
self.update_card(&mut card, &orig)?;
self.update_card(&mut card, &orig, self.usn()?)?;
Ok(card)
}
pub(crate) fn update_card(&mut self, card: &mut Card, original: &Card) -> Result<()> {
pub(crate) fn update_card(&mut self, card: &mut Card, original: &Card, usn: Usn) -> Result<()> {
if card.id.0 == 0 {
return Err(AnkiError::invalid_input("card id not set"));
}
self.state
.undo
.save_undoable(Box::new(UpdateCardUndo(original.clone())));
card.mtime = TimestampSecs::now();
card.usn = self.usn()?;
card.set_modified(usn);
self.storage.update_card(card)
}

View file

@ -0,0 +1,133 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
backend_proto as pb,
card::{Card, CardID, CardQueue, CardType},
collection::Collection,
err::Result,
};
use super::cutoff::SchedTimingToday;
use pb::unbury_cards_in_current_deck_in::Mode as UnburyDeckMode;
impl Card {
/// True if card was buried/suspended prior to the call.
pub(crate) fn restore_queue_after_bury_or_suspend(&mut self) -> bool {
if !matches!(
self.queue,
CardQueue::Suspended | CardQueue::SchedBuried | CardQueue::UserBuried
) {
false
} else {
self.queue = match self.ctype {
CardType::Learn | CardType::Relearn => {
let original_due = if self.odue > 0 { self.odue } else { self.due };
if original_due > 1_000_000_000 {
// previous interval was in seconds
CardQueue::Learn
} else {
// previous interval was in days
CardQueue::DayLearn
}
}
CardType::New => CardQueue::New,
CardType::Review => CardQueue::Review,
};
true
}
}
}
impl Collection {
pub(crate) fn unbury_if_day_rolled_over(&mut self, timing: SchedTimingToday) -> Result<()> {
let last_unburied = self.get_last_unburied_day();
let today = timing.days_elapsed;
if last_unburied < today || (today + 7) < last_unburied {
self.unbury_on_day_rollover()?;
self.set_last_unburied_day(today)?;
}
Ok(())
}
/// Unbury cards from the previous day.
/// Done automatically, and does not mark the cards as modified.
fn unbury_on_day_rollover(&mut self) -> Result<()> {
self.search_cards_into_table("is:buried")?;
self.storage.for_each_card_in_search(|mut card| {
card.restore_queue_after_bury_or_suspend();
self.storage.update_card(&card)
})?;
self.clear_searched_cards()
}
/// Unsuspend/unbury cards in search table, and clear it.
/// Marks the cards as modified.
fn unsuspend_or_unbury_searched_cards(&mut self) -> Result<()> {
let usn = self.usn()?;
for original in self.storage.all_searched_cards()? {
let mut card = original.clone();
if card.restore_queue_after_bury_or_suspend() {
self.update_card(&mut card, &original, usn)?;
}
}
self.clear_searched_cards()
}
pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> {
self.transact(None, |col| {
col.set_search_table_to_card_ids(cids)?;
col.unsuspend_or_unbury_searched_cards()
})
}
pub fn unbury_cards_in_current_deck(&mut self, mode: UnburyDeckMode) -> Result<()> {
let search = match mode {
UnburyDeckMode::All => "is:buried",
UnburyDeckMode::UserOnly => "is:buried-manually",
UnburyDeckMode::SchedOnly => "is:buried-sibling",
};
self.transact(None, |col| {
col.search_cards_into_table(&format!("deck:current {}", search))?;
col.unsuspend_or_unbury_searched_cards()
})
}
}
#[cfg(test)]
mod test {
use crate::{
card::{Card, CardQueue},
collection::{open_test_collection, Collection},
search::SortMode,
};
#[test]
fn unbury() {
let mut col = open_test_collection();
let mut card = Card::default();
card.queue = CardQueue::UserBuried;
col.add_card(&mut card).unwrap();
let assert_count = |col: &mut Collection, cnt| {
assert_eq!(
col.search_cards("is:buried", SortMode::NoOrder)
.unwrap()
.len(),
cnt
);
};
assert_count(&mut col, 1);
// day 0, last unburied 0, so no change
let timing = col.timing_today().unwrap();
col.unbury_if_day_rolled_over(timing).unwrap();
assert_count(&mut col, 1);
// move creation time back and it should succeed
let mut stamp = col.storage.creation_stamp().unwrap();
stamp.0 -= 86_400;
col.storage.set_creation_stamp(stamp).unwrap();
let timing = col.timing_today().unwrap();
col.unbury_if_day_rolled_over(timing).unwrap();
assert_count(&mut col, 0);
}
}

View file

@ -5,6 +5,7 @@ use crate::{
collection::Collection, config::SchedulerVersion, err::Result, timestamp::TimestampSecs,
};
pub mod bury_and_suspend;
pub(crate) mod congrats;
pub mod cutoff;
pub mod timespan;
@ -79,61 +80,4 @@ impl Collection {
SchedulerVersion::V2 => self.set_v2_rollover(hour as u32),
}
}
pub(crate) fn unbury_if_day_rolled_over(&mut self) -> Result<()> {
let last_unburied = self.get_last_unburied_day();
let today = self.timing_today()?.days_elapsed;
if last_unburied < today || (today + 7) < last_unburied {
self.unbury_on_day_rollover()?;
self.set_last_unburied_day(today)?;
}
Ok(())
}
fn unbury_on_day_rollover(&mut self) -> Result<()> {
self.search_cards_into_table("is:buried")?;
self.storage.for_each_card_in_search(|mut card| {
card.restore_queue_after_bury_or_suspend();
self.storage.update_card(&card)
})?;
self.clear_searched_cards()?;
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::{
card::{Card, CardQueue},
collection::{open_test_collection, Collection},
search::SortMode,
};
#[test]
fn unbury() {
let mut col = open_test_collection();
let mut card = Card::default();
card.queue = CardQueue::UserBuried;
col.add_card(&mut card).unwrap();
let assert_count = |col: &mut Collection, cnt| {
assert_eq!(
col.search_cards("is:buried", SortMode::NoOrder)
.unwrap()
.len(),
cnt
);
};
assert_count(&mut col, 1);
// day 0, last unburied 0, so no change
col.unbury_if_day_rolled_over().unwrap();
assert_count(&mut col, 1);
// move creation time back and it should succeed
let mut stamp = col.storage.creation_stamp().unwrap();
stamp.0 -= 86_400;
col.storage.set_creation_stamp(stamp).unwrap();
col.unbury_if_day_rolled_over().unwrap();
assert_count(&mut col, 0);
}
}

View file

@ -5,12 +5,10 @@ use super::{
parser::Node,
sqlwriter::{RequiredTable, SqlWriter},
};
use crate::card::CardID;
use crate::card::CardType;
use crate::collection::Collection;
use crate::config::SortKind;
use crate::err::Result;
use crate::search::parser::parse;
use crate::{
card::CardID, card::CardType, collection::Collection, config::SortKind, err::Result,
search::parser::parse,
};
use rusqlite::NO_PARAMS;
#[derive(Debug, PartialEq, Clone)]
@ -95,10 +93,9 @@ impl Collection {
let writer = SqlWriter::new(self);
let (sql, args) = writer.build_cards_query(&top_node, RequiredTable::Cards)?;
self.storage.db.execute_batch(concat!(
"drop table if exists search_cids;",
"create temporary table search_cids (id integer primary key not null);"
))?;
self.storage
.db
.execute_batch(include_str!("search_cids_setup.sql"))?;
let sql = format!("insert into search_cids {}", sql);
self.storage.db.prepare(&sql)?.execute(&args)?;
@ -106,6 +103,24 @@ impl Collection {
Ok(())
}
/// Injects the provided card IDs into the search_cids table, for
/// when ids have arrived outside of a search.
/// Clear with clear_searched_cards().
pub(crate) fn set_search_table_to_card_ids(&mut self, cards: &[CardID]) -> Result<()> {
self.storage
.db
.execute_batch(include_str!("search_cids_setup.sql"))?;
let mut stmt = self
.storage
.db
.prepare_cached("insert into search_cids values (?)")?;
for cid in cards {
stmt.execute(&[cid])?;
}
Ok(())
}
pub(crate) fn clear_searched_cards(&self) -> Result<()> {
self.storage
.db

View file

@ -0,0 +1,2 @@
drop table if exists search_cids;
create temporary table search_cids (id integer primary key not null);

View file

@ -279,7 +279,7 @@ impl super::SqliteStorage {
Ok(())
}
pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result<CongratsInfo> {
self.update_active_decks(current)?;
self.db

View file

@ -319,7 +319,8 @@ where
SyncActionRequired::FullSyncRequired { .. } => Ok(state.into()),
SyncActionRequired::NormalSyncRequired => {
self.col.storage.begin_trx()?;
self.col.unbury_if_day_rolled_over()?;
self.col
.unbury_if_day_rolled_over(self.col.timing_today()?)?;
match self.normal_sync_inner(state).await {
Ok(success) => {
self.col.storage.commit_trx()?;

View file

@ -4,11 +4,13 @@
use crate::{
collection::{Collection, CollectionOp},
err::Result,
types::Usn,
};
use std::fmt;
pub(crate) trait Undoable: fmt::Debug + Send {
fn apply(&self, ctx: &mut Collection) -> Result<()>;
/// Undo the recorded action.
fn apply(&self, ctx: &mut Collection, usn: Usn) -> Result<()>;
}
#[derive(Debug)]
@ -97,8 +99,9 @@ impl Collection {
let changes = step.changes;
self.state.undo.mode = UndoMode::Undoing;
let res = self.transact(Some(step.kind), |col| {
let usn = col.usn()?;
for change in changes.iter().rev() {
change.apply(col)?;
change.apply(col, usn)?;
}
Ok(())
});
@ -113,8 +116,9 @@ impl Collection {
let changes = step.changes;
self.state.undo.mode = UndoMode::Redoing;
let res = self.transact(Some(step.kind), |col| {
let usn = col.usn()?;
for change in changes.iter().rev() {
change.apply(col)?;
change.apply(col, usn)?;
}
Ok(())
});