diff --git a/proto/backend.proto b/proto/backend.proto index a38b59472..cb5f4bccc 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -102,6 +102,7 @@ service BackendService { rpc CongratsInfo (Empty) returns (CongratsInfoOut); rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty); rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty); + rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty); // stats @@ -156,6 +157,7 @@ service BackendService { rpc AfterNoteUpdates (AfterNoteUpdatesIn) returns (Empty); rpc FieldNamesForNotes (FieldNamesForNotesIn) returns (FieldNamesForNotesOut); rpc NoteIsDuplicateOrEmpty (Note) returns (NoteIsDuplicateOrEmptyOut); + rpc CardsOfNote (NoteID) returns (CardIDs); // note types @@ -1042,3 +1044,13 @@ message UnburyCardsInCurrentDeckIn { } Mode mode = 1; } + +message BuryOrSuspendCardsIn { + enum Mode { + SUSPEND = 0; + BURY_SCHED = 1; + BURY_USER = 2; + } + repeated int64 card_ids = 1; + Mode mode = 2; +} diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index f66ab90e6..320a6565b 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -356,6 +356,9 @@ class Collection: hooks.notes_will_be_deleted(self, nids) 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 def addNote(self, note: Note) -> int: diff --git a/pylib/anki/notes.py b/pylib/anki/notes.py index 0871a761f..b265e5edc 100644 --- a/pylib/anki/notes.py +++ b/pylib/anki/notes.py @@ -76,12 +76,10 @@ class Note: return joinFields(self.fields) def cards(self) -> List[anki.cards.Card]: - return [ - self.col.getCard(id) - for id in self.col.db.list( - "select id from cards where nid = ? order by ord", self.id - ) - ] + return [self.col.getCard(id) for id in self.card_ids()] + + def card_ids(self) -> Sequence[int]: + return self.col.card_ids_of_note(self.id) def model(self) -> Optional[NoteType]: return self.col.models.get(self.mid) diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 9f44e9b3f..924972b34 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -806,32 +806,3 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" return self._graduatingIvl(card, conf, False, adj=False) * 86400 else: 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(), - ) diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index fedf002b1..7ca006b83 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -27,6 +27,7 @@ from anki.cards import Card from anki.consts import * from anki.decks import Deck, DeckConfig, DeckManager, FilteredDeck, QueueConfig from anki.lang import _ +from anki.notes import Note from anki.rsbackend import ( CountsForDeckToday, DeckTreeNode, @@ -36,10 +37,14 @@ from anki.rsbackend import ( ) 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: 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 @@ -1387,34 +1392,20 @@ where id = ? ) -> None: self.col.backend.unbury_cards_in_current_deck(mode) - def suspendCards(self, ids: List[int]) -> None: - "Suspend cards." - self.col.log(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 suspend_cards(self, ids: Sequence[int]) -> None: + self.col.backend.bury_or_suspend_cards( + card_ids=ids, mode=BuryOrSuspendMode.SUSPEND ) - 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) - self.col.db.execute( - """ -update cards set queue=?,mod=?,usn=? where id in """ - + ids2str(cids), - queue, - intTime(), - self.col.usn(), - ) + def bury_cards(self, ids: Sequence[int], manual: bool = True) -> None: + if manual: + mode = BuryOrSuspendMode.BURY_USER + else: + mode = BuryOrSuspendMode.BURY_SCHED + self.col.backend.bury_or_suspend_cards(card_ids=ids, mode=mode) - def buryNote(self, nid: int) -> None: - "Bury all cards for note until next session." - cids = self.col.db.list( - f"select id from cards where nid = ? and queue >= {QUEUE_TYPE_NEW}", nid - ) - self.buryCards(cids) + def bury_note(self, note: Note): + self.bury_cards(note.card_ids()) # legacy @@ -1424,7 +1415,14 @@ update cards set queue=?,mod=?,usn=? where id in """ ) 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: + print( + "please use unbury_cards_in_current_deck() instead of unburyCardsForDeck()" + ) if type == "all": mode = UnburyCurrentDeckMode.ALL elif type == "manual": @@ -1434,6 +1432,8 @@ update cards set queue=?,mod=?,usn=? where id in """ self.unbury_cards_in_current_deck(mode) unsuspendCards = unsuspend_cards + buryCards = bury_cards + suspendCards = suspend_cards # Sibling spacing ########################################################################## @@ -1469,7 +1469,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", pass # then bury if toBury: - self.buryCards(toBury, manual=False) + self.bury_cards(toBury, manual=False) # Resetting ########################################################################## diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index 4144b2f2d..025ec55de 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -500,7 +500,7 @@ def test_misc(): col.addNote(note) c = note.cards()[0] # burying - col.sched.buryNote(c.nid) + col.sched.bury_note(note) col.reset() assert not col.sched.getCard() col.sched.unbury_cards_in_current_deck() @@ -517,11 +517,11 @@ def test_suspend(): # suspending col.reset() assert col.sched.getCard() - col.sched.suspendCards([c.id]) + col.sched.suspend_cards([c.id]) col.reset() assert not col.sched.getCard() # unsuspending - col.sched.unsuspendCards([c.id]) + col.sched.unsuspend_cards([c.id]) col.reset() assert col.sched.getCard() # should cope with rev cards being relearnt @@ -536,8 +536,8 @@ def test_suspend(): assert c.due >= time.time() assert c.queue == QUEUE_TYPE_LRN assert c.type == CARD_TYPE_REV - col.sched.suspendCards([c.id]) - col.sched.unsuspendCards([c.id]) + col.sched.suspend_cards([c.id]) + col.sched.unsuspend_cards([c.id]) c.load() assert c.queue == QUEUE_TYPE_REV assert c.type == CARD_TYPE_REV @@ -550,7 +550,7 @@ def test_suspend(): c.load() assert c.due != 1 assert c.did != 1 - col.sched.suspendCards([c.id]) + col.sched.suspend_cards([c.id]) c.load() assert c.due == 1 assert c.did == 1 diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 14296a59f..6e928ae7b 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -600,10 +600,12 @@ def test_bury(): col.addNote(note) c2 = note.cards()[0] # 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() 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() assert c2.queue == QUEUE_TYPE_SIBLING_BURIED @@ -620,7 +622,7 @@ def test_bury(): c2.load() 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.reset() @@ -637,11 +639,11 @@ def test_suspend(): # suspending col.reset() assert col.sched.getCard() - col.sched.suspendCards([c.id]) + col.sched.suspend_cards([c.id]) col.reset() assert not col.sched.getCard() # unsuspending - col.sched.unsuspendCards([c.id]) + col.sched.unsuspend_cards([c.id]) col.reset() assert col.sched.getCard() # should cope with rev cards being relearnt @@ -657,8 +659,8 @@ def test_suspend(): due = c.due assert c.queue == QUEUE_TYPE_LRN assert c.type == CARD_TYPE_RELEARNING - col.sched.suspendCards([c.id]) - col.sched.unsuspendCards([c.id]) + col.sched.suspend_cards([c.id]) + col.sched.unsuspend_cards([c.id]) c.load() assert c.queue == QUEUE_TYPE_LRN assert c.type == CARD_TYPE_RELEARNING @@ -671,7 +673,7 @@ def test_suspend(): c.load() assert c.due != 1 assert c.did != 1 - col.sched.suspendCards([c.id]) + col.sched.suspend_cards([c.id]) c.load() assert c.due != 1 assert c.did != 1 @@ -1199,7 +1201,7 @@ def test_moveVersions(): col.reset() c = col.sched.getCard() col.sched.answerCard(c, 1) - col.sched.buryCards([c.id]) + col.sched.bury_cards([c.id]) c.load() assert c.queue == QUEUE_TYPE_MANUALLY_BURIED diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 5bde1ccf2..816b85f42 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1672,9 +1672,9 @@ update cards set usn=?, mod=?, did=? where id in """ sus = not self.isSuspended() c = self.selectedCards() if sus: - self.col.sched.suspendCards(c) + self.col.sched.suspend_cards(c) else: - self.col.sched.unsuspendCards(c) + self.col.sched.unsuspend_cards(c) self.model.reset() self.mw.requireReset(reason=ResetReason.BrowserSuspend, context=self) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 2e4979057..c63c9a668 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -794,13 +794,13 @@ time = %(time)d; def onSuspend(self) -> None: 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.")) self.mw.reset() def onSuspendCard(self) -> None: self.mw.checkpoint(_("Suspend")) - self.mw.col.sched.suspendCards([self.card.id]) + self.mw.col.sched.suspend_cards([self.card.id]) tooltip(_("Card suspended.")) self.mw.reset() @@ -822,13 +822,13 @@ time = %(time)d; def onBuryCard(self) -> None: self.mw.checkpoint(_("Bury")) - self.mw.col.sched.buryCards([self.card.id]) + self.mw.col.sched.bury_cards([self.card.id]) self.mw.reset() tooltip(_("Card buried.")) def onBuryNote(self) -> None: self.mw.checkpoint(_("Bury")) - self.mw.col.sched.buryNote(self.card.nid) + self.mw.col.sched.bury_note(self.card.note()) self.mw.reset() tooltip(_("Note buried.")) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 8a168d59e..b90e31754 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -531,6 +531,14 @@ impl BackendService for Backend { }) } + fn bury_or_suspend_cards(&mut self, input: pb::BuryOrSuspendCardsIn) -> BackendResult { + 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 //----------------------------------------------- @@ -880,6 +888,16 @@ impl BackendService for Backend { }) } + fn cards_of_note(&mut self, input: pb::NoteId) -> BackendResult { + 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 //------------------------------------------------------------------- diff --git a/rslib/src/card.rs b/rslib/src/card.rs index e7c275371..b6884096f 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -6,8 +6,8 @@ use crate::define_newtype; use crate::err::{AnkiError, Result}; use crate::notes::NoteID; use crate::{ - collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn, - undo::Undoable, + collection::Collection, config::SchedulerVersion, deckconf::INITIAL_EASE_FACTOR, + timestamp::TimestampSecs, types::Usn, undo::Undoable, }; use num_enum::TryFromPrimitive; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -104,11 +104,10 @@ impl Card { pub(crate) fn return_home(&mut self, sched: SchedulerVersion) { if self.odid.0 == 0 { - // this should not happen + // not in a filtered deck return; } - // fixme: avoid bumping mtime? self.did = self.odid; self.odid.0 = 0; if self.odue > 0 { @@ -154,6 +153,32 @@ impl Card { 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)] pub(crate) struct UpdateCardUndo(Card); diff --git a/rslib/src/deckconf/mod.rs b/rslib/src/deckconf/mod.rs index f57fe4158..4235acbfa 100644 --- a/rslib/src/deckconf/mod.rs +++ b/rslib/src/deckconf/mod.rs @@ -14,6 +14,7 @@ pub use crate::backend_proto::{ DeckConfigInner, }; pub use schema11::{DeckConfSchema11, NewCardOrderSchema11}; +pub const INITIAL_EASE_FACTOR: u16 = 2500; mod schema11; diff --git a/rslib/src/deckconf/schema11.rs b/rslib/src/deckconf/schema11.rs index 585fbfe55..5c9e0039e 100644 --- a/rslib/src/deckconf/schema11.rs +++ b/rslib/src/deckconf/schema11.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // 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::DeckConfigInner; use crate::{serde::default_on_invalid, timestamp::TimestampSecs, types::Usn}; @@ -153,7 +153,7 @@ impl Default for NewConfSchema11 { NewConfSchema11 { bury: false, delays: vec![1.0, 10.0], - initial_factor: 2500, + initial_factor: INITIAL_EASE_FACTOR, ints: NewCardIntervals::default(), order: NewCardOrderSchema11::default(), per_day: 20, diff --git a/rslib/src/sched/bury_and_suspend.rs b/rslib/src/sched/bury_and_suspend.rs index 98410b19a..11cce7bdf 100644 --- a/rslib/src/sched/bury_and_suspend.rs +++ b/rslib/src/sched/bury_and_suspend.rs @@ -5,6 +5,7 @@ use crate::{ backend_proto as pb, card::{Card, CardID, CardQueue, CardType}, collection::Collection, + config::SchedulerVersion, err::Result, }; @@ -93,6 +94,54 @@ impl Collection { 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)] diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 4117244c5..8535ab88f 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -237,6 +237,13 @@ impl super::SqliteStorage { .collect() } + pub(crate) fn all_card_ids_of_note(&self, nid: NoteID) -> Result> { + 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> { let mut stmt = self .db