set_deck()

This commit is contained in:
Damien Elmes 2020-09-03 17:42:46 +10:00
parent 9214c4a700
commit 56ceb6ba76
13 changed files with 167 additions and 139 deletions

View file

@ -150,6 +150,7 @@ service BackendService {
rpc UpdateCard (Card) returns (Empty); rpc UpdateCard (Card) returns (Empty);
rpc AddCard (Card) returns (CardID); rpc AddCard (Card) returns (CardID);
rpc RemoveCards (RemoveCardsIn) returns (Empty); rpc RemoveCards (RemoveCardsIn) returns (Empty);
rpc SetDeck (SetDeckIn) returns (Empty);
// notes // notes
@ -1081,3 +1082,8 @@ message SortDeckIn {
int64 deck_id = 1; int64 deck_id = 1;
bool randomize = 2; bool randomize = 2;
} }
message SetDeckIn {
repeated int64 card_ids = 1;
int64 deck_id = 2;
}

View file

@ -384,6 +384,9 @@ class Collection:
"You probably want .remove_notes_by_card() instead." "You probably want .remove_notes_by_card() instead."
self.backend.remove_cards(card_ids=card_ids) 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 # legacy
def remCards(self, ids: List[int], notes: bool = True) -> None: def remCards(self, ids: List[int], notes: bool = True) -> None:

View file

@ -22,7 +22,7 @@ from anki.models import NoteType
from anki.notes import Note from anki.notes import Note
from anki.rsbackend import TR, DeckTreeNode, InvalidInput from anki.rsbackend import TR, DeckTreeNode, InvalidInput
from anki.stats import CardStats 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 import AnkiQt, gui_hooks
from aqt.editor import Editor from aqt.editor import Editor
from aqt.exporting import ExportDialog from aqt.exporting import ExportDialog
@ -1601,21 +1601,7 @@ where id in %s"""
return return
self.model.beginReset() self.model.beginReset()
self.mw.checkpoint(_("Change Deck")) self.mw.checkpoint(_("Change Deck"))
mod = intTime() self.col.set_deck(cids, did)
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.model.endReset() self.model.endReset()
self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self) self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self)

View file

@ -817,6 +817,12 @@ impl BackendService for Backend {
}) })
} }
fn set_deck(&mut self, input: pb::SetDeckIn) -> BackendResult<Empty> {
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 // notes
//------------------------------------------------------------------- //-------------------------------------------------------------------

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 crate::decks::{DeckFilterContext, DeckID}; use crate::decks::DeckID;
use crate::define_newtype; use crate::define_newtype;
use crate::err::{AnkiError, Result}; use crate::err::{AnkiError, Result};
use crate::notes::NoteID; use crate::notes::NoteID;
@ -102,103 +102,10 @@ impl Card {
self.usn = usn; self.usn = usn;
} }
pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) { /// Caller must ensure provided deck exists and is not filtered.
// filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero fn set_deck(&mut self, deck: DeckID, sched: SchedulerVersion) {
if self.original_due != 0 { self.remove_from_filtered_deck_restoring_queue(sched);
println!("bug: odue was set"); self.deck_id = deck;
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;
} }
} }
#[derive(Debug)] #[derive(Debug)]
@ -289,6 +196,27 @@ impl Collection {
Ok(()) 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)] #[cfg(test)]

View file

@ -17,11 +17,9 @@ use crate::{
types::Usn, types::Usn,
}; };
mod counts; mod counts;
mod filtered;
mod schema11; mod schema11;
mod tree; mod tree;
pub(crate) use counts::DueCounts; pub(crate) use counts::DueCounts;
pub(crate) use filtered::DeckFilterContext;
pub use schema11::DeckSchema11; pub use schema11::DeckSchema11;
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};

View file

@ -1,13 +1,13 @@
// 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::{Deck, DeckID};
pub use crate::backend_proto::{ pub use crate::backend_proto::{
deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto, deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto,
DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck,
}; };
use crate::decks::{Deck, DeckID};
use crate::{ use crate::{
card::{CardID, CardQueue}, card::{Card, CardID, CardQueue, CardType},
collection::Collection, collection::Collection,
config::SchedulerVersion, config::SchedulerVersion,
err::Result, err::Result,
@ -17,6 +17,107 @@ use crate::{
types::Usn, 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 { impl Deck {
pub fn new_filtered() -> Deck { pub fn new_filtered() -> Deck {
let mut filt = FilteredDeck::default(); let mut filt = FilteredDeck::default();

View file

@ -13,6 +13,7 @@ pub mod dbcheck;
pub mod deckconf; pub mod deckconf;
pub mod decks; pub mod decks;
pub mod err; pub mod err;
pub mod filtered;
pub mod findreplace; pub mod findreplace;
pub mod i18n; pub mod i18n;
pub mod latex; pub mod latex;

View file

@ -83,7 +83,7 @@ impl Collection {
pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> { pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> {
self.transact(None, |col| { 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() col.unsuspend_or_unbury_searched_cards()
}) })
} }
@ -143,7 +143,7 @@ impl Collection {
mode: pb::bury_or_suspend_cards_in::Mode, mode: pb::bury_or_suspend_cards_in::Mode,
) -> Result<()> { ) -> Result<()> {
self.transact(None, |col| { 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) col.bury_or_suspend_searched_cards(mode)
}) })
} }

View file

@ -72,7 +72,7 @@ impl Collection {
let usn = self.usn()?; let usn = self.usn()?;
let mut position = self.get_next_card_position(); let mut position = self.get_next_card_position();
self.transact(None, |col| { 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()?; let cards = col.storage.all_searched_cards()?;
for mut card in cards { for mut card in cards {
let original = card.clone(); let original = card.clone();
@ -113,7 +113,7 @@ impl Collection {
if shift { if shift {
self.shift_existing_cards(starting_from, step * cids.len() as u32, usn)?; 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 cards = self.storage.all_searched_cards()?;
let sorter = NewCardSorter::new(&cards, starting_from, step, random); let sorter = NewCardSorter::new(&cards, starting_from, step, random);
for mut card in cards { for mut card in cards {

View file

@ -36,7 +36,7 @@ impl Collection {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let distribution = Uniform::from(min_days..=max_days); let distribution = Uniform::from(min_days..=max_days);
self.transact(None, |col| { 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()? { for mut card in col.storage.all_searched_cards()? {
let original = card.clone(); let original = card.clone();
let interval = distribution.sample(&mut rng); let interval = distribution.sample(&mut rng);

View file

@ -106,22 +106,6 @@ impl Collection {
Ok(()) 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. /// If the sort mode is based on a config setting, look it up.
fn resolve_config_sort(&self, mode: &mut SortMode) { fn resolve_config_sort(&self, mode: &mut SortMode) {
if mode == &SortMode::FromConfig { if mode == &SortMode::FromConfig {

View file

@ -335,6 +335,21 @@ impl super::SqliteStorage {
.execute("drop table if exists search_cids", NO_PARAMS)?; .execute("drop table if exists search_cids", NO_PARAMS)?;
Ok(()) 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)] #[cfg(test)]