move bury/suspend into backend

This commit is contained in:
Damien Elmes 2020-08-31 16:14:04 +10:00
parent ac265fe75a
commit d3dede057a
15 changed files with 176 additions and 90 deletions

View file

@ -102,6 +102,7 @@ service BackendService {
rpc CongratsInfo (Empty) returns (CongratsInfoOut); rpc CongratsInfo (Empty) returns (CongratsInfoOut);
rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty); rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty);
rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty); rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty);
rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty);
// stats // stats
@ -156,6 +157,7 @@ service BackendService {
rpc AfterNoteUpdates (AfterNoteUpdatesIn) returns (Empty); rpc AfterNoteUpdates (AfterNoteUpdatesIn) returns (Empty);
rpc FieldNamesForNotes (FieldNamesForNotesIn) returns (FieldNamesForNotesOut); rpc FieldNamesForNotes (FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
rpc NoteIsDuplicateOrEmpty (Note) returns (NoteIsDuplicateOrEmptyOut); rpc NoteIsDuplicateOrEmpty (Note) returns (NoteIsDuplicateOrEmptyOut);
rpc CardsOfNote (NoteID) returns (CardIDs);
// note types // note types
@ -1042,3 +1044,13 @@ message UnburyCardsInCurrentDeckIn {
} }
Mode mode = 1; Mode mode = 1;
} }
message BuryOrSuspendCardsIn {
enum Mode {
SUSPEND = 0;
BURY_SCHED = 1;
BURY_USER = 2;
}
repeated int64 card_ids = 1;
Mode mode = 2;
}

View file

@ -356,6 +356,9 @@ class Collection:
hooks.notes_will_be_deleted(self, nids) hooks.notes_will_be_deleted(self, nids)
self.backend.remove_notes(note_ids=[], card_ids=card_ids) self.backend.remove_notes(note_ids=[], card_ids=card_ids)
def card_ids_of_note(self, note_id: int) -> Sequence[int]:
return self.backend.cards_of_note(note_id)
# legacy # legacy
def addNote(self, note: Note) -> int: def addNote(self, note: Note) -> int:

View file

@ -76,12 +76,10 @@ class Note:
return joinFields(self.fields) return joinFields(self.fields)
def cards(self) -> List[anki.cards.Card]: def cards(self) -> List[anki.cards.Card]:
return [ return [self.col.getCard(id) for id in self.card_ids()]
self.col.getCard(id)
for id in self.col.db.list( def card_ids(self) -> Sequence[int]:
"select id from cards where nid = ? order by ord", self.id return self.col.card_ids_of_note(self.id)
)
]
def model(self) -> Optional[NoteType]: def model(self) -> Optional[NoteType]:
return self.col.models.get(self.mid) return self.col.models.get(self.mid)

View file

@ -806,32 +806,3 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
return self._graduatingIvl(card, conf, False, adj=False) * 86400 return self._graduatingIvl(card, conf, False, adj=False) * 86400
else: else:
return self._delayForGrade(conf, left) return self._delayForGrade(conf, left)
# Suspending
##########################################################################
def suspendCards(self, ids: List[int]) -> None:
"Suspend cards."
self.col.log(ids)
self.remFromDyn(ids)
self.removeLrn(ids)
self.col.db.execute(
f"update cards set queue={QUEUE_TYPE_SUSPENDED},mod=?,usn=? where 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
self.col.log(cids)
self.remFromDyn(cids)
self.removeLrn(cids)
self.col.db.execute(
f"""
update cards set queue={QUEUE_TYPE_SIBLING_BURIED},mod=?,usn=? where id in """
+ ids2str(cids),
intTime(),
self.col.usn(),
)

View file

@ -27,6 +27,7 @@ from anki.cards import Card
from anki.consts import * from anki.consts import *
from anki.decks import Deck, DeckConfig, DeckManager, FilteredDeck, QueueConfig from anki.decks import Deck, DeckConfig, DeckManager, FilteredDeck, QueueConfig
from anki.lang import _ from anki.lang import _
from anki.notes import Note
from anki.rsbackend import ( from anki.rsbackend import (
CountsForDeckToday, CountsForDeckToday,
DeckTreeNode, DeckTreeNode,
@ -36,10 +37,14 @@ from anki.rsbackend import (
) )
from anki.utils import ids2str, intTime from anki.utils import ids2str, intTime
UnburyCurrentDeckMode = pb.UnburyCardsInCurrentDeckIn.Mode # pylint: disable=no-member UnburyCurrentDeckMode = pb.UnburyCardsInCurrentDeckIn.Mode # pylint:disable=no-member
BuryOrSuspendMode = pb.BuryOrSuspendCardsIn.Mode # pylint:disable=no-member
if TYPE_CHECKING: if TYPE_CHECKING:
UnburyCurrentDeckModeValue = ( UnburyCurrentDeckModeValue = (
pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint: disable=no-member pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint:disable=no-member
)
BuryOrSuspendModeValue = (
pb.BuryOrSuspendCardsIn.ModeValue # pylint:disable=no-member
) )
# card types: 0=new, 1=lrn, 2=rev, 3=relrn # card types: 0=new, 1=lrn, 2=rev, 3=relrn
@ -1387,34 +1392,20 @@ where id = ?
) -> None: ) -> None:
self.col.backend.unbury_cards_in_current_deck(mode) self.col.backend.unbury_cards_in_current_deck(mode)
def suspendCards(self, ids: List[int]) -> None: def suspend_cards(self, ids: Sequence[int]) -> None:
"Suspend cards." self.col.backend.bury_or_suspend_cards(
self.col.log(ids) card_ids=ids, mode=BuryOrSuspendMode.SUSPEND
self.col.db.execute(
f"update cards set queue={QUEUE_TYPE_SUSPENDED},mod=?,usn=? where id in "
+ ids2str(ids),
intTime(),
self.col.usn(),
) )
def buryCards(self, cids: List[int], manual: bool = True) -> None: def bury_cards(self, ids: Sequence[int], manual: bool = True) -> None:
queue = manual and QUEUE_TYPE_MANUALLY_BURIED or QUEUE_TYPE_SIBLING_BURIED if manual:
self.col.log(cids) mode = BuryOrSuspendMode.BURY_USER
self.col.db.execute( else:
""" mode = BuryOrSuspendMode.BURY_SCHED
update cards set queue=?,mod=?,usn=? where id in """ self.col.backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
+ ids2str(cids),
queue,
intTime(),
self.col.usn(),
)
def buryNote(self, nid: int) -> None: def bury_note(self, note: Note):
"Bury all cards for note until next session." self.bury_cards(note.card_ids())
cids = self.col.db.list(
f"select id from cards where nid = ? and queue >= {QUEUE_TYPE_NEW}", nid
)
self.buryCards(cids)
# legacy # legacy
@ -1424,7 +1415,14 @@ update cards set queue=?,mod=?,usn=? where id in """
) )
self.unbury_cards_in_current_deck() self.unbury_cards_in_current_deck()
def buryNote(self, nid: int) -> None:
note = self.col.getNote(nid)
self.bury_cards(note.card_ids())
def unburyCardsForDeck(self, type: str = "all") -> None: def unburyCardsForDeck(self, type: str = "all") -> None:
print(
"please use unbury_cards_in_current_deck() instead of unburyCardsForDeck()"
)
if type == "all": if type == "all":
mode = UnburyCurrentDeckMode.ALL mode = UnburyCurrentDeckMode.ALL
elif type == "manual": elif type == "manual":
@ -1434,6 +1432,8 @@ update cards set queue=?,mod=?,usn=? where id in """
self.unbury_cards_in_current_deck(mode) self.unbury_cards_in_current_deck(mode)
unsuspendCards = unsuspend_cards unsuspendCards = unsuspend_cards
buryCards = bury_cards
suspendCards = suspend_cards
# Sibling spacing # Sibling spacing
########################################################################## ##########################################################################
@ -1469,7 +1469,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
pass pass
# then bury # then bury
if toBury: if toBury:
self.buryCards(toBury, manual=False) self.bury_cards(toBury, manual=False)
# Resetting # Resetting
########################################################################## ##########################################################################

View file

@ -500,7 +500,7 @@ def test_misc():
col.addNote(note) col.addNote(note)
c = note.cards()[0] c = note.cards()[0]
# burying # burying
col.sched.buryNote(c.nid) col.sched.bury_note(note)
col.reset() col.reset()
assert not col.sched.getCard() assert not col.sched.getCard()
col.sched.unbury_cards_in_current_deck() col.sched.unbury_cards_in_current_deck()
@ -517,11 +517,11 @@ def test_suspend():
# suspending # suspending
col.reset() col.reset()
assert col.sched.getCard() assert col.sched.getCard()
col.sched.suspendCards([c.id]) col.sched.suspend_cards([c.id])
col.reset() col.reset()
assert not col.sched.getCard() assert not col.sched.getCard()
# unsuspending # unsuspending
col.sched.unsuspendCards([c.id]) col.sched.unsuspend_cards([c.id])
col.reset() col.reset()
assert col.sched.getCard() assert col.sched.getCard()
# should cope with rev cards being relearnt # should cope with rev cards being relearnt
@ -536,8 +536,8 @@ def test_suspend():
assert c.due >= time.time() assert c.due >= time.time()
assert c.queue == QUEUE_TYPE_LRN assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_REV assert c.type == CARD_TYPE_REV
col.sched.suspendCards([c.id]) col.sched.suspend_cards([c.id])
col.sched.unsuspendCards([c.id]) col.sched.unsuspend_cards([c.id])
c.load() c.load()
assert c.queue == QUEUE_TYPE_REV assert c.queue == QUEUE_TYPE_REV
assert c.type == CARD_TYPE_REV assert c.type == CARD_TYPE_REV
@ -550,7 +550,7 @@ def test_suspend():
c.load() c.load()
assert c.due != 1 assert c.due != 1
assert c.did != 1 assert c.did != 1
col.sched.suspendCards([c.id]) col.sched.suspend_cards([c.id])
c.load() c.load()
assert c.due == 1 assert c.due == 1
assert c.did == 1 assert c.did == 1

View file

@ -600,10 +600,12 @@ def test_bury():
col.addNote(note) col.addNote(note)
c2 = note.cards()[0] c2 = note.cards()[0]
# burying # burying
col.sched.buryCards([c.id], manual=True) # pylint: disable=unexpected-keyword-arg col.sched.bury_cards([c.id], manual=True) # pylint: disable=unexpected-keyword-arg
c.load() c.load()
assert c.queue == QUEUE_TYPE_MANUALLY_BURIED assert c.queue == QUEUE_TYPE_MANUALLY_BURIED
col.sched.buryCards([c2.id], manual=False) # pylint: disable=unexpected-keyword-arg col.sched.bury_cards(
[c2.id], manual=False
) # pylint: disable=unexpected-keyword-arg
c2.load() c2.load()
assert c2.queue == QUEUE_TYPE_SIBLING_BURIED assert c2.queue == QUEUE_TYPE_SIBLING_BURIED
@ -620,7 +622,7 @@ def test_bury():
c2.load() c2.load()
assert c2.queue == QUEUE_TYPE_NEW assert c2.queue == QUEUE_TYPE_NEW
col.sched.buryCards([c.id, c2.id]) col.sched.bury_cards([c.id, c2.id])
col.sched.unbury_cards_in_current_deck() col.sched.unbury_cards_in_current_deck()
col.reset() col.reset()
@ -637,11 +639,11 @@ def test_suspend():
# suspending # suspending
col.reset() col.reset()
assert col.sched.getCard() assert col.sched.getCard()
col.sched.suspendCards([c.id]) col.sched.suspend_cards([c.id])
col.reset() col.reset()
assert not col.sched.getCard() assert not col.sched.getCard()
# unsuspending # unsuspending
col.sched.unsuspendCards([c.id]) col.sched.unsuspend_cards([c.id])
col.reset() col.reset()
assert col.sched.getCard() assert col.sched.getCard()
# should cope with rev cards being relearnt # should cope with rev cards being relearnt
@ -657,8 +659,8 @@ def test_suspend():
due = c.due due = c.due
assert c.queue == QUEUE_TYPE_LRN assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_RELEARNING assert c.type == CARD_TYPE_RELEARNING
col.sched.suspendCards([c.id]) col.sched.suspend_cards([c.id])
col.sched.unsuspendCards([c.id]) col.sched.unsuspend_cards([c.id])
c.load() c.load()
assert c.queue == QUEUE_TYPE_LRN assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_RELEARNING assert c.type == CARD_TYPE_RELEARNING
@ -671,7 +673,7 @@ def test_suspend():
c.load() c.load()
assert c.due != 1 assert c.due != 1
assert c.did != 1 assert c.did != 1
col.sched.suspendCards([c.id]) col.sched.suspend_cards([c.id])
c.load() c.load()
assert c.due != 1 assert c.due != 1
assert c.did != 1 assert c.did != 1
@ -1199,7 +1201,7 @@ def test_moveVersions():
col.reset() col.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
col.sched.buryCards([c.id]) col.sched.bury_cards([c.id])
c.load() c.load()
assert c.queue == QUEUE_TYPE_MANUALLY_BURIED assert c.queue == QUEUE_TYPE_MANUALLY_BURIED

View file

@ -1672,9 +1672,9 @@ update cards set usn=?, mod=?, did=? where id in """
sus = not self.isSuspended() sus = not self.isSuspended()
c = self.selectedCards() c = self.selectedCards()
if sus: if sus:
self.col.sched.suspendCards(c) self.col.sched.suspend_cards(c)
else: else:
self.col.sched.unsuspendCards(c) self.col.sched.unsuspend_cards(c)
self.model.reset() self.model.reset()
self.mw.requireReset(reason=ResetReason.BrowserSuspend, context=self) self.mw.requireReset(reason=ResetReason.BrowserSuspend, context=self)

View file

@ -794,13 +794,13 @@ time = %(time)d;
def onSuspend(self) -> None: def onSuspend(self) -> None:
self.mw.checkpoint(_("Suspend")) self.mw.checkpoint(_("Suspend"))
self.mw.col.sched.suspendCards([c.id for c in self.card.note().cards()]) self.mw.col.sched.suspend_cards([c.id for c in self.card.note().cards()])
tooltip(_("Note suspended.")) tooltip(_("Note suspended."))
self.mw.reset() self.mw.reset()
def onSuspendCard(self) -> None: def onSuspendCard(self) -> None:
self.mw.checkpoint(_("Suspend")) self.mw.checkpoint(_("Suspend"))
self.mw.col.sched.suspendCards([self.card.id]) self.mw.col.sched.suspend_cards([self.card.id])
tooltip(_("Card suspended.")) tooltip(_("Card suspended."))
self.mw.reset() self.mw.reset()
@ -822,13 +822,13 @@ time = %(time)d;
def onBuryCard(self) -> None: def onBuryCard(self) -> None:
self.mw.checkpoint(_("Bury")) self.mw.checkpoint(_("Bury"))
self.mw.col.sched.buryCards([self.card.id]) self.mw.col.sched.bury_cards([self.card.id])
self.mw.reset() self.mw.reset()
tooltip(_("Card buried.")) tooltip(_("Card buried."))
def onBuryNote(self) -> None: def onBuryNote(self) -> None:
self.mw.checkpoint(_("Bury")) self.mw.checkpoint(_("Bury"))
self.mw.col.sched.buryNote(self.card.nid) self.mw.col.sched.bury_note(self.card.note())
self.mw.reset() self.mw.reset()
tooltip(_("Note buried.")) tooltip(_("Note buried."))

View file

@ -531,6 +531,14 @@ impl BackendService for Backend {
}) })
} }
fn bury_or_suspend_cards(&mut self, input: pb::BuryOrSuspendCardsIn) -> BackendResult<Empty> {
self.with_col(|col| {
let mode = input.mode();
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
col.bury_or_suspend_cards(&cids, mode).map(Into::into)
})
}
// statistics // statistics
//----------------------------------------------- //-----------------------------------------------
@ -880,6 +888,16 @@ impl BackendService for Backend {
}) })
} }
fn cards_of_note(&mut self, input: pb::NoteId) -> BackendResult<pb::CardIDs> {
self.with_col(|col| {
col.storage
.all_card_ids_of_note(NoteID(input.nid))
.map(|v| pb::CardIDs {
cids: v.into_iter().map(Into::into).collect(),
})
})
}
// notetypes // notetypes
//------------------------------------------------------------------- //-------------------------------------------------------------------

View file

@ -6,8 +6,8 @@ use crate::define_newtype;
use crate::err::{AnkiError, Result}; use crate::err::{AnkiError, Result};
use crate::notes::NoteID; use crate::notes::NoteID;
use crate::{ use crate::{
collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn, collection::Collection, config::SchedulerVersion, deckconf::INITIAL_EASE_FACTOR,
undo::Undoable, timestamp::TimestampSecs, types::Usn, undo::Undoable,
}; };
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
@ -104,11 +104,10 @@ impl Card {
pub(crate) fn return_home(&mut self, sched: SchedulerVersion) { pub(crate) fn return_home(&mut self, sched: SchedulerVersion) {
if self.odid.0 == 0 { if self.odid.0 == 0 {
// this should not happen // not in a filtered deck
return; return;
} }
// fixme: avoid bumping mtime?
self.did = self.odid; self.did = self.odid;
self.odid.0 = 0; self.odid.0 = 0;
if self.odue > 0 { if self.odue > 0 {
@ -154,6 +153,32 @@ impl Card {
self.ctype = CardType::New; self.ctype = CardType::New;
} }
} }
/// Remove the card from the (re)learning queue.
/// This will reset cards in learning.
/// Only used in the V1 scheduler.
/// Unlike the legacy Python code, this sets the due# to 0 instead of
/// one past the previous max due number.
pub(crate) fn remove_from_learning(&mut self) {
if !matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn) {
return;
}
if self.ctype == CardType::Review {
// reviews are removed from relearning
self.due = self.odue;
self.odue = 0;
self.queue = CardQueue::Review;
} else {
// other cards are reset to new
self.ctype = CardType::New;
self.queue = CardQueue::New;
self.ivl = 0;
self.due = 0;
self.odue = 0;
self.factor = INITIAL_EASE_FACTOR;
}
}
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct UpdateCardUndo(Card); pub(crate) struct UpdateCardUndo(Card);

View file

@ -14,6 +14,7 @@ pub use crate::backend_proto::{
DeckConfigInner, DeckConfigInner,
}; };
pub use schema11::{DeckConfSchema11, NewCardOrderSchema11}; pub use schema11::{DeckConfSchema11, NewCardOrderSchema11};
pub const INITIAL_EASE_FACTOR: u16 = 2500;
mod schema11; mod schema11;

View file

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// 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
use super::{DeckConf, DeckConfID}; use super::{DeckConf, DeckConfID, INITIAL_EASE_FACTOR};
use crate::backend_proto::deck_config_inner::NewCardOrder; use crate::backend_proto::deck_config_inner::NewCardOrder;
use crate::backend_proto::DeckConfigInner; use crate::backend_proto::DeckConfigInner;
use crate::{serde::default_on_invalid, timestamp::TimestampSecs, types::Usn}; use crate::{serde::default_on_invalid, timestamp::TimestampSecs, types::Usn};
@ -153,7 +153,7 @@ impl Default for NewConfSchema11 {
NewConfSchema11 { NewConfSchema11 {
bury: false, bury: false,
delays: vec![1.0, 10.0], delays: vec![1.0, 10.0],
initial_factor: 2500, initial_factor: INITIAL_EASE_FACTOR,
ints: NewCardIntervals::default(), ints: NewCardIntervals::default(),
order: NewCardOrderSchema11::default(), order: NewCardOrderSchema11::default(),
per_day: 20, per_day: 20,

View file

@ -5,6 +5,7 @@ use crate::{
backend_proto as pb, backend_proto as pb,
card::{Card, CardID, CardQueue, CardType}, card::{Card, CardID, CardQueue, CardType},
collection::Collection, collection::Collection,
config::SchedulerVersion,
err::Result, err::Result,
}; };
@ -93,6 +94,54 @@ impl Collection {
col.unsuspend_or_unbury_searched_cards() col.unsuspend_or_unbury_searched_cards()
}) })
} }
/// Bury/suspend cards in search table, and clear it.
/// Marks the cards as modified.
fn bury_or_suspend_searched_cards(
&mut self,
mode: pb::bury_or_suspend_cards_in::Mode,
) -> Result<()> {
use pb::bury_or_suspend_cards_in::Mode;
let usn = self.usn()?;
let sched = self.sched_ver();
for original in self.storage.all_searched_cards()? {
let mut card = original.clone();
let desired_queue = match mode {
Mode::Suspend => CardQueue::Suspended,
Mode::BurySched => CardQueue::SchedBuried,
Mode::BuryUser => {
if sched == SchedulerVersion::V1 {
// v1 scheduler only had one bury type
CardQueue::SchedBuried
} else {
CardQueue::UserBuried
}
}
};
if card.queue != desired_queue {
if sched == SchedulerVersion::V1 {
card.return_home(sched);
card.remove_from_learning();
}
card.queue = desired_queue;
self.update_card(&mut card, &original, usn)?;
}
}
self.clear_searched_cards()
}
pub fn bury_or_suspend_cards(
&mut self,
cids: &[CardID],
mode: pb::bury_or_suspend_cards_in::Mode,
) -> Result<()> {
self.transact(None, |col| {
col.set_search_table_to_card_ids(cids)?;
col.bury_or_suspend_searched_cards(mode)
})
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -237,6 +237,13 @@ impl super::SqliteStorage {
.collect() .collect()
} }
pub(crate) fn all_card_ids_of_note(&self, nid: NoteID) -> Result<Vec<CardID>> {
self.db
.prepare_cached("select id from cards where nid = ? order by ord")?
.query_and_then(&[nid], |r| Ok(CardID(r.get(0)?)))?
.collect()
}
pub(crate) fn note_ids_of_cards(&self, cids: &[CardID]) -> Result<HashSet<NoteID>> { pub(crate) fn note_ids_of_cards(&self, cids: &[CardID]) -> Result<HashSet<NoteID>> {
let mut stmt = self let mut stmt = self
.db .db