diff --git a/proto/backend.proto b/proto/backend.proto index bac9d8a9e..b6fba6f84 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -105,6 +105,7 @@ service BackendService { rpc BuryOrSuspendCards (BuryOrSuspendCardsIn) returns (Empty); rpc EmptyFilteredDeck (DeckID) returns (Empty); rpc RebuildFilteredDeck (DeckID) returns (UInt32); + rpc ScheduleCardsAsReviews (ScheduleCardsAsReviewsIn) returns (Empty); // stats @@ -1056,3 +1057,9 @@ message BuryOrSuspendCardsIn { repeated int64 card_ids = 1; Mode mode = 2; } + +message ScheduleCardsAsReviewsIn { + repeated int64 card_ids = 1; + uint32 min_interval = 2; + uint32 max_interval = 3; +} diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 53ab369fb..87b2e373d 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -1419,29 +1419,9 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", def reschedCards(self, ids: List[int], imin: int, imax: int) -> None: "Put cards in review queue with a new interval in days (min, max)." - d = [] - t = self.today - mod = intTime() - for id in ids: - r = random.randint(imin, imax) - d.append( - ( - max(1, r), - r + t, - self.col.usn(), - mod, - STARTING_FACTOR, - id, - ) - ) - self.remFromDyn(ids) - self.col.db.executemany( - f""" -update cards set type={CARD_TYPE_REV},queue={QUEUE_TYPE_REV},ivl=?,due=?,odue=0, -usn=?,mod=?,factor=? where id=?""", - d, + self.col.backend.schedule_cards_as_reviews( + card_ids=ids, min_interval=imin, max_interval=imax ) - self.col.log(ids) def resetCards(self, ids: List[int]) -> None: "Completely reset cards for export." diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 977a1dce2..755baacbf 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -547,6 +547,18 @@ impl BackendService for Backend { self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into)) } + fn schedule_cards_as_reviews( + &mut self, + input: pb::ScheduleCardsAsReviewsIn, + ) -> BackendResult { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let (min, max) = (input.min_interval, input.max_interval); + self.with_col(|col| { + col.reschedule_cards_as_reviews(&cids, min, max) + .map(Into::into) + }) + } + // statistics //----------------------------------------------- diff --git a/rslib/src/card.rs b/rslib/src/card.rs index 65ada96c5..e50235ec3 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -137,7 +137,18 @@ impl Card { } } - pub(crate) fn remove_from_filtered_deck(&mut self, sched: SchedulerVersion) { + /// 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; diff --git a/rslib/src/decks/filtered.rs b/rslib/src/decks/filtered.rs index c93de02ce..0f9a278d8 100644 --- a/rslib/src/decks/filtered.rs +++ b/rslib/src/decks/filtered.rs @@ -66,7 +66,7 @@ impl Collection { for cid in cids { if let Some(mut card) = self.storage.get_card(*cid)? { let original = card.clone(); - card.remove_from_filtered_deck(sched); + card.remove_from_filtered_deck_restoring_queue(sched); self.update_card(&mut card, &original, usn)?; } } diff --git a/rslib/src/sched/bury_and_suspend.rs b/rslib/src/sched/bury_and_suspend.rs index a8201ead9..e14979328 100644 --- a/rslib/src/sched/bury_and_suspend.rs +++ b/rslib/src/sched/bury_and_suspend.rs @@ -126,7 +126,7 @@ impl Collection { }; if card.queue != desired_queue { if sched == SchedulerVersion::V1 { - card.remove_from_filtered_deck(sched); + card.remove_from_filtered_deck_restoring_queue(sched); card.remove_from_learning(); } card.queue = desired_queue; diff --git a/rslib/src/sched/mod.rs b/rslib/src/sched/mod.rs index f7c03a840..af0df2997 100644 --- a/rslib/src/sched/mod.rs +++ b/rslib/src/sched/mod.rs @@ -8,6 +8,7 @@ use crate::{ pub mod bury_and_suspend; pub(crate) mod congrats; pub mod cutoff; +mod reviews; pub mod timespan; use chrono::FixedOffset; diff --git a/rslib/src/sched/reviews.rs b/rslib/src/sched/reviews.rs new file mode 100644 index 000000000..0ad8062b3 --- /dev/null +++ b/rslib/src/sched/reviews.rs @@ -0,0 +1,50 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + card::{Card, CardID, CardQueue, CardType}, + collection::Collection, + deckconf::INITIAL_EASE_FACTOR, + err::Result, +}; +use rand::distributions::{Distribution, Uniform}; + +impl Card { + fn schedule_as_review(&mut self, interval: u32, today: u32) { + self.remove_from_filtered_deck_before_reschedule(); + self.interval = interval.max(1); + self.due = (today + interval) as i32; + self.ctype = CardType::Review; + self.queue = CardQueue::Review; + if self.ease_factor == 0 { + // unlike the old Python code, we leave the ease factor alone + // if it's already set + self.ease_factor = INITIAL_EASE_FACTOR; + } + } +} + +impl Collection { + pub fn reschedule_cards_as_reviews( + &mut self, + cids: &[CardID], + min_days: u32, + max_days: u32, + ) -> Result<()> { + let usn = self.usn()?; + let today = self.timing_today()?.days_elapsed; + 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)?; + for mut card in col.storage.all_searched_cards()? { + let original = card.clone(); + let interval = distribution.sample(&mut rng); + card.schedule_as_review(interval, today); + col.update_card(&mut card, &original, usn)?; + } + col.clear_searched_cards()?; + Ok(()) + }) + } +}