From 56ceb6ba76a44d8e0aeeb07d8c3256819e59efae Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 3 Sep 2020 17:42:46 +1000 Subject: [PATCH] set_deck() --- proto/backend.proto | 6 ++ pylib/anki/collection.py | 3 + qt/aqt/browser.py | 18 +--- rslib/src/backend/mod.rs | 6 ++ rslib/src/card.rs | 124 ++++++---------------------- rslib/src/decks/mod.rs | 2 - rslib/src/{decks => }/filtered.rs | 105 ++++++++++++++++++++++- rslib/src/lib.rs | 1 + rslib/src/sched/bury_and_suspend.rs | 4 +- rslib/src/sched/new.rs | 4 +- rslib/src/sched/reviews.rs | 2 +- rslib/src/search/cards.rs | 16 ---- rslib/src/storage/card/mod.rs | 15 ++++ 13 files changed, 167 insertions(+), 139 deletions(-) rename rslib/src/{decks => }/filtered.rs (58%) diff --git a/proto/backend.proto b/proto/backend.proto index 483f69fc1..b438cca38 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -150,6 +150,7 @@ service BackendService { rpc UpdateCard (Card) returns (Empty); rpc AddCard (Card) returns (CardID); rpc RemoveCards (RemoveCardsIn) returns (Empty); + rpc SetDeck (SetDeckIn) returns (Empty); // notes @@ -1081,3 +1082,8 @@ message SortDeckIn { int64 deck_id = 1; bool randomize = 2; } + +message SetDeckIn { + repeated int64 card_ids = 1; + int64 deck_id = 2; +} diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index d423f42a9..cf39340e8 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -384,6 +384,9 @@ class Collection: "You probably want .remove_notes_by_card() instead." self.backend.remove_cards(card_ids=card_ids) + def set_deck(self, card_ids: List[int], deck_id: int) -> None: + self.backend.set_deck(card_ids=card_ids, deck_id=deck_id) + # legacy def remCards(self, ids: List[int], notes: bool = True) -> None: diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 816b85f42..d36909606 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -22,7 +22,7 @@ from anki.models import NoteType from anki.notes import Note from anki.rsbackend import TR, DeckTreeNode, InvalidInput from anki.stats import CardStats -from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin +from anki.utils import htmlToTextLine, ids2str, isMac, isWin from aqt import AnkiQt, gui_hooks from aqt.editor import Editor from aqt.exporting import ExportDialog @@ -1601,21 +1601,7 @@ where id in %s""" return self.model.beginReset() self.mw.checkpoint(_("Change Deck")) - mod = intTime() - usn = self.col.usn() - # normal cards - scids = ids2str(cids) - # remove any cards from filtered deck first - self.col.sched.remFromDyn(cids) - # then move into new deck - self.col.db.execute( - """ -update cards set usn=?, mod=?, did=? where id in """ - + scids, - usn, - mod, - did, - ) + self.col.set_deck(cids, did) self.model.endReset() self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 281f37bbe..3741bf93c 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -817,6 +817,12 @@ impl BackendService for Backend { }) } + fn set_deck(&mut self, input: pb::SetDeckIn) -> BackendResult { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let deck_id = input.deck_id.into(); + self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into)) + } + // notes //------------------------------------------------------------------- diff --git a/rslib/src/card.rs b/rslib/src/card.rs index 42404c21b..4841adeb2 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.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 crate::decks::{DeckFilterContext, DeckID}; +use crate::decks::DeckID; use crate::define_newtype; use crate::err::{AnkiError, Result}; use crate::notes::NoteID; @@ -102,103 +102,10 @@ impl Card { self.usn = usn; } - pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) { - // filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero - if self.original_due != 0 { - println!("bug: odue was set"); - return; - } - - self.original_deck_id = self.deck_id; - self.deck_id = ctx.target_deck; - - self.original_due = self.due; - - if ctx.scheduler == SchedulerVersion::V1 { - if self.ctype == CardType::Review && self.due <= ctx.today as i32 { - // review cards that are due are left in the review queue - } else { - // new + non-due go into new queue - self.queue = CardQueue::New; - } - if self.due != 0 { - self.due = position; - } - } else { - // if rescheduling is disabled, all cards go in the review queue - if !ctx.config.reschedule { - self.queue = CardQueue::Review; - } - // fixme: can we unify this with v1 scheduler in the future? - // https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745 - if self.due > 0 { - self.due = position; - } - } - } - - /// Restores to the original deck and clears original_due. - /// This does not update the queue or type, so should only be used as - /// part of an operation that adjusts those separately. - pub(crate) fn remove_from_filtered_deck_before_reschedule(&mut self) { - if self.original_deck_id.0 != 0 { - self.deck_id = self.original_deck_id; - self.original_deck_id.0 = 0; - self.original_due = 0; - } - } - - pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) { - if self.original_deck_id.0 == 0 { - // not in a filtered deck - return; - } - - self.deck_id = self.original_deck_id; - self.original_deck_id.0 = 0; - - match sched { - SchedulerVersion::V1 => { - self.due = self.original_due; - self.queue = match self.ctype { - CardType::New => CardQueue::New, - CardType::Learn => CardQueue::New, - CardType::Review => CardQueue::Review, - // not applicable in v1, should not happen - CardType::Relearn => { - println!("did not expect relearn type in v1 for card {}", self.id); - CardQueue::New - } - }; - if self.ctype == CardType::Learn { - self.ctype = CardType::New; - } - } - SchedulerVersion::V2 => { - // original_due is cleared if card answered in filtered deck - if self.original_due > 0 { - self.due = self.original_due; - } - - if (self.queue as i8) >= 0 { - self.queue = match self.ctype { - CardType::Learn | CardType::Relearn => { - if self.due > 1_000_000_000 { - // unix timestamp - CardQueue::Learn - } else { - // day number - CardQueue::DayLearn - } - } - CardType::New => CardQueue::New, - CardType::Review => CardQueue::Review, - } - } - } - } - - self.original_due = 0; + /// Caller must ensure provided deck exists and is not filtered. + fn set_deck(&mut self, deck: DeckID, sched: SchedulerVersion) { + self.remove_from_filtered_deck_restoring_queue(sched); + self.deck_id = deck; } } #[derive(Debug)] @@ -289,6 +196,27 @@ impl Collection { Ok(()) } + + pub fn set_deck(&mut self, cards: &[CardID], deck_id: DeckID) -> Result<()> { + let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; + if deck.is_filtered() { + return Err(AnkiError::DeckIsFiltered); + } + self.storage.set_search_table_to_card_ids(cards)?; + let sched = self.sched_ver(); + let usn = self.usn()?; + self.transact(None, |col| { + for mut card in col.storage.all_searched_cards()? { + if card.deck_id == deck_id { + continue; + } + let original = card.clone(); + card.set_deck(deck_id, sched); + col.update_card(&mut card, &original, usn)?; + } + Ok(()) + }) + } } #[cfg(test)] diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index af7db1845..a0ea612a3 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -17,11 +17,9 @@ use crate::{ types::Usn, }; mod counts; -mod filtered; mod schema11; mod tree; pub(crate) use counts::DueCounts; -pub(crate) use filtered::DeckFilterContext; pub use schema11::DeckSchema11; use std::{borrow::Cow, sync::Arc}; diff --git a/rslib/src/decks/filtered.rs b/rslib/src/filtered.rs similarity index 58% rename from rslib/src/decks/filtered.rs rename to rslib/src/filtered.rs index 0f9a278d8..fca4422e9 100644 --- a/rslib/src/decks/filtered.rs +++ b/rslib/src/filtered.rs @@ -1,13 +1,13 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::{Deck, DeckID}; pub use crate::backend_proto::{ deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto, DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, }; +use crate::decks::{Deck, DeckID}; use crate::{ - card::{CardID, CardQueue}, + card::{Card, CardID, CardQueue, CardType}, collection::Collection, config::SchedulerVersion, err::Result, @@ -17,6 +17,107 @@ use crate::{ types::Usn, }; +impl Card { + pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) { + // filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero + if self.original_due != 0 { + println!("bug: odue was set"); + return; + } + + self.original_deck_id = self.deck_id; + self.deck_id = ctx.target_deck; + + self.original_due = self.due; + + if ctx.scheduler == SchedulerVersion::V1 { + if self.ctype == CardType::Review && self.due <= ctx.today as i32 { + // review cards that are due are left in the review queue + } else { + // new + non-due go into new queue + self.queue = CardQueue::New; + } + if self.due != 0 { + self.due = position; + } + } else { + // if rescheduling is disabled, all cards go in the review queue + if !ctx.config.reschedule { + self.queue = CardQueue::Review; + } + // fixme: can we unify this with v1 scheduler in the future? + // https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745 + if self.due > 0 { + self.due = position; + } + } + } + + /// Restores to the original deck and clears original_due. + /// This does not update the queue or type, so should only be used as + /// part of an operation that adjusts those separately. + pub(crate) fn remove_from_filtered_deck_before_reschedule(&mut self) { + if self.original_deck_id.0 != 0 { + self.deck_id = self.original_deck_id; + self.original_deck_id.0 = 0; + self.original_due = 0; + } + } + + pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) { + if self.original_deck_id.0 == 0 { + // not in a filtered deck + return; + } + + self.deck_id = self.original_deck_id; + self.original_deck_id.0 = 0; + + match sched { + SchedulerVersion::V1 => { + self.due = self.original_due; + self.queue = match self.ctype { + CardType::New => CardQueue::New, + CardType::Learn => CardQueue::New, + CardType::Review => CardQueue::Review, + // not applicable in v1, should not happen + CardType::Relearn => { + println!("did not expect relearn type in v1 for card {}", self.id); + CardQueue::New + } + }; + if self.ctype == CardType::Learn { + self.ctype = CardType::New; + } + } + SchedulerVersion::V2 => { + // original_due is cleared if card answered in filtered deck + if self.original_due > 0 { + self.due = self.original_due; + } + + if (self.queue as i8) >= 0 { + self.queue = match self.ctype { + CardType::Learn | CardType::Relearn => { + if self.due > 1_000_000_000 { + // unix timestamp + CardQueue::Learn + } else { + // day number + CardQueue::DayLearn + } + } + CardType::New => CardQueue::New, + CardType::Review => CardQueue::Review, + } + } + } + } + + self.original_due = 0; + } +} + impl Deck { pub fn new_filtered() -> Deck { let mut filt = FilteredDeck::default(); diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index 574569f58..1cca25c30 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -13,6 +13,7 @@ pub mod dbcheck; pub mod deckconf; pub mod decks; pub mod err; +pub mod filtered; pub mod findreplace; pub mod i18n; pub mod latex; diff --git a/rslib/src/sched/bury_and_suspend.rs b/rslib/src/sched/bury_and_suspend.rs index a51c3e416..01fdd0ad0 100644 --- a/rslib/src/sched/bury_and_suspend.rs +++ b/rslib/src/sched/bury_and_suspend.rs @@ -83,7 +83,7 @@ impl Collection { pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> { self.transact(None, |col| { - col.set_search_table_to_card_ids(cids)?; + col.storage.set_search_table_to_card_ids(cids)?; col.unsuspend_or_unbury_searched_cards() }) } @@ -143,7 +143,7 @@ impl Collection { mode: pb::bury_or_suspend_cards_in::Mode, ) -> Result<()> { self.transact(None, |col| { - col.set_search_table_to_card_ids(cids)?; + col.storage.set_search_table_to_card_ids(cids)?; col.bury_or_suspend_searched_cards(mode) }) } diff --git a/rslib/src/sched/new.rs b/rslib/src/sched/new.rs index 7ae5ac197..dc7bb9e68 100644 --- a/rslib/src/sched/new.rs +++ b/rslib/src/sched/new.rs @@ -72,7 +72,7 @@ impl Collection { let usn = self.usn()?; let mut position = self.get_next_card_position(); self.transact(None, |col| { - col.set_search_table_to_card_ids(cids)?; + col.storage.set_search_table_to_card_ids(cids)?; let cards = col.storage.all_searched_cards()?; for mut card in cards { let original = card.clone(); @@ -113,7 +113,7 @@ impl Collection { if shift { self.shift_existing_cards(starting_from, step * cids.len() as u32, usn)?; } - self.set_search_table_to_card_ids(cids)?; + self.storage.set_search_table_to_card_ids(cids)?; let cards = self.storage.all_searched_cards()?; let sorter = NewCardSorter::new(&cards, starting_from, step, random); for mut card in cards { diff --git a/rslib/src/sched/reviews.rs b/rslib/src/sched/reviews.rs index 160a23faa..182cbe08e 100644 --- a/rslib/src/sched/reviews.rs +++ b/rslib/src/sched/reviews.rs @@ -36,7 +36,7 @@ impl Collection { let mut rng = rand::thread_rng(); let distribution = Uniform::from(min_days..=max_days); self.transact(None, |col| { - col.set_search_table_to_card_ids(cids)?; + col.storage.set_search_table_to_card_ids(cids)?; for mut card in col.storage.all_searched_cards()? { let original = card.clone(); let interval = distribution.sample(&mut rng); diff --git a/rslib/src/search/cards.rs b/rslib/src/search/cards.rs index 5f9f5fbd4..fba1a752b 100644 --- a/rslib/src/search/cards.rs +++ b/rslib/src/search/cards.rs @@ -106,22 +106,6 @@ 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.setup_searched_cards_table()?; - let mut stmt = self - .storage - .db - .prepare_cached("insert into search_cids values (?)")?; - for cid in cards { - stmt.execute(&[cid])?; - } - - Ok(()) - } - /// If the sort mode is based on a config setting, look it up. fn resolve_config_sort(&self, mode: &mut SortMode) { if mode == &SortMode::FromConfig { diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index de7640678..f001e1f3b 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -335,6 +335,21 @@ impl super::SqliteStorage { .execute("drop table if exists search_cids", NO_PARAMS)?; 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.setup_searched_cards_table()?; + let mut stmt = self + .db + .prepare_cached("insert into search_cids values (?)")?; + for cid in cards { + stmt.execute(&[cid])?; + } + + Ok(()) + } } #[cfg(test)]