mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
move card sorting and resetting to backend
This commit is contained in:
parent
e56f83be84
commit
b65174a026
13 changed files with 245 additions and 87 deletions
|
@ -107,6 +107,9 @@ service BackendService {
|
|||
rpc EmptyFilteredDeck (DeckID) returns (Empty);
|
||||
rpc RebuildFilteredDeck (DeckID) returns (UInt32);
|
||||
rpc ScheduleCardsAsReviews (ScheduleCardsAsReviewsIn) returns (Empty);
|
||||
rpc ScheduleCardsAsNew (CardIDs) returns (Empty);
|
||||
rpc SortCards (SortCardsIn) returns (Empty);
|
||||
rpc SortDeck (SortDeckIn) returns (Empty);
|
||||
|
||||
// stats
|
||||
|
||||
|
@ -1065,3 +1068,16 @@ message ScheduleCardsAsReviewsIn {
|
|||
uint32 min_interval = 2;
|
||||
uint32 max_interval = 3;
|
||||
}
|
||||
|
||||
message SortCardsIn {
|
||||
repeated int64 card_ids = 1;
|
||||
uint32 starting_from = 2;
|
||||
uint32 step_size = 3;
|
||||
bool randomize = 4;
|
||||
bool shift_existing = 5;
|
||||
}
|
||||
|
||||
message SortDeckIn {
|
||||
int64 deck_id = 1;
|
||||
bool randomize = 2;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ from typing import (
|
|||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
@ -1401,26 +1400,16 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
|||
# Resetting
|
||||
##########################################################################
|
||||
|
||||
def forgetCards(self, ids: List[int]) -> None:
|
||||
def schedule_cards_as_new(self, card_ids: List[int]) -> None:
|
||||
"Put cards at the end of the new queue."
|
||||
self.remFromDyn(ids)
|
||||
self.col.db.execute(
|
||||
f"update cards set type={CARD_TYPE_NEW},queue={QUEUE_TYPE_NEW},ivl=0,due=0,odue=0,factor=?"
|
||||
" where id in " + ids2str(ids),
|
||||
STARTING_FACTOR,
|
||||
)
|
||||
pmax = (
|
||||
self.col.db.scalar(f"select max(due) from cards where type={CARD_TYPE_NEW}")
|
||||
or 0
|
||||
)
|
||||
# takes care of mod + usn
|
||||
self.sortCards(ids, start=pmax + 1)
|
||||
self.col.log(ids)
|
||||
self.col.backend.schedule_cards_as_new(card_ids)
|
||||
|
||||
def reschedCards(self, ids: List[int], imin: int, imax: int) -> None:
|
||||
"Put cards in review queue with a new interval in days (min, max)."
|
||||
def schedule_cards_as_reviews(
|
||||
self, card_ids: List[int], min_interval: int, max_interval: int
|
||||
) -> None:
|
||||
"Make cards review cards, with a new interval randomly selected from range."
|
||||
self.col.backend.schedule_cards_as_reviews(
|
||||
card_ids=ids, min_interval=imin, max_interval=imax
|
||||
card_ids=card_ids, min_interval=min_interval, max_interval=max_interval
|
||||
)
|
||||
|
||||
def resetCards(self, ids: List[int]) -> None:
|
||||
|
@ -1440,6 +1429,11 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
|||
self.forgetCards(nonNew)
|
||||
self.col.log(ids)
|
||||
|
||||
# legacy
|
||||
|
||||
forgetCards = schedule_cards_as_new
|
||||
reschedCards = schedule_cards_as_reviews
|
||||
|
||||
# Repositioning new cards
|
||||
##########################################################################
|
||||
|
||||
|
@ -1451,60 +1445,19 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
|||
shuffle: bool = False,
|
||||
shift: bool = False,
|
||||
) -> None:
|
||||
scids = ids2str(cids)
|
||||
now = intTime()
|
||||
nids = []
|
||||
nidsSet: Set[int] = set()
|
||||
for id in cids:
|
||||
nid = self.col.db.scalar("select nid from cards where id = ?", id)
|
||||
if nid not in nidsSet:
|
||||
nids.append(nid)
|
||||
nidsSet.add(nid)
|
||||
if not nids:
|
||||
# no new cards
|
||||
return
|
||||
# determine nid ordering
|
||||
due = {}
|
||||
if shuffle:
|
||||
random.shuffle(nids)
|
||||
for c, nid in enumerate(nids):
|
||||
due[nid] = start + c * step
|
||||
# pylint: disable=undefined-loop-variable
|
||||
high = start + c * step
|
||||
# shift?
|
||||
if shift:
|
||||
low = self.col.db.scalar(
|
||||
f"select min(due) from cards where due >= ? and type = {CARD_TYPE_NEW} "
|
||||
"and id not in %s" % scids,
|
||||
start,
|
||||
)
|
||||
if low is not None:
|
||||
shiftby = high - low + 1
|
||||
self.col.db.execute(
|
||||
f"""
|
||||
update cards set mod=?, usn=?, due=due+? where id not in %s
|
||||
and due >= ? and queue = {QUEUE_TYPE_NEW}"""
|
||||
% scids,
|
||||
now,
|
||||
self.col.usn(),
|
||||
shiftby,
|
||||
low,
|
||||
)
|
||||
# reorder cards
|
||||
d = []
|
||||
for id, nid in self.col.db.execute(
|
||||
f"select id, nid from cards where type = {CARD_TYPE_NEW} and id in " + scids
|
||||
):
|
||||
d.append((due[nid], now, self.col.usn(), id))
|
||||
self.col.db.executemany("update cards set due=?,mod=?,usn=? where id = ?", d)
|
||||
self.col.backend.sort_cards(
|
||||
card_ids=cids,
|
||||
starting_from=start,
|
||||
step_size=step,
|
||||
randomize=shuffle,
|
||||
shift_existing=shift,
|
||||
)
|
||||
|
||||
def randomizeCards(self, did: int) -> None:
|
||||
cids = self.col.db.list("select id from cards where did = ?", did)
|
||||
self.sortCards(cids, shuffle=True)
|
||||
self.col.backend.sort_deck(deck_id=did, randomize=True)
|
||||
|
||||
def orderCards(self, did: int) -> None:
|
||||
cids = self.col.db.list("select id from cards where did = ? order by nid", did)
|
||||
self.sortCards(cids)
|
||||
self.col.backend.sort_deck(deck_id=did, randomize=False)
|
||||
|
||||
def resortConf(self, conf) -> None:
|
||||
for did in self.col.decks.didsForConf(conf):
|
||||
|
|
|
@ -569,6 +569,34 @@ impl BackendService for Backend {
|
|||
})
|
||||
}
|
||||
|
||||
fn schedule_cards_as_new(&mut self, input: pb::CardIDs) -> BackendResult<Empty> {
|
||||
self.with_col(|col| {
|
||||
col.reschedule_cards_as_new(&input.into_native())
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
fn sort_cards(&mut self, input: pb::SortCardsIn) -> BackendResult<Empty> {
|
||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
||||
let (start, step, random, shift) = (
|
||||
input.starting_from,
|
||||
input.step_size,
|
||||
input.randomize,
|
||||
input.shift_existing,
|
||||
);
|
||||
self.with_col(|col| {
|
||||
col.sort_cards(&cids, start, step, random, shift)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
fn sort_deck(&mut self, input: pb::SortDeckIn) -> BackendResult<Empty> {
|
||||
self.with_col(|col| {
|
||||
col.sort_deck(input.deck_id.into(), input.randomize)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
// statistics
|
||||
//-----------------------------------------------
|
||||
|
||||
|
|
|
@ -179,6 +179,10 @@ impl Collection {
|
|||
self.set_config(ConfigKey::CurrentNoteTypeID, &id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_next_card_position(&self) -> u32 {
|
||||
self.get_config_default(ConfigKey::NextNewCardPosition)
|
||||
}
|
||||
|
||||
pub(crate) fn get_and_update_next_card_position(&self) -> Result<u32> {
|
||||
let pos: u32 = self
|
||||
.get_config_optional(ConfigKey::NextNewCardPosition)
|
||||
|
|
|
@ -65,7 +65,7 @@ impl Collection {
|
|||
card.restore_queue_after_bury_or_suspend();
|
||||
self.storage.update_card(&card)
|
||||
})?;
|
||||
self.clear_searched_cards()
|
||||
self.storage.clear_searched_cards_table()
|
||||
}
|
||||
|
||||
/// Unsuspend/unbury cards in search table, and clear it.
|
||||
|
@ -78,7 +78,7 @@ impl Collection {
|
|||
self.update_card(&mut card, &original, usn)?;
|
||||
}
|
||||
}
|
||||
self.clear_searched_cards()
|
||||
self.storage.clear_searched_cards_table()
|
||||
}
|
||||
|
||||
pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> {
|
||||
|
@ -134,7 +134,7 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
self.clear_searched_cards()
|
||||
self.storage.clear_searched_cards_table()
|
||||
}
|
||||
|
||||
pub fn bury_or_suspend_cards(
|
||||
|
|
|
@ -9,6 +9,7 @@ pub mod bury_and_suspend;
|
|||
pub(crate) mod congrats;
|
||||
pub mod cutoff;
|
||||
mod learning;
|
||||
pub mod new;
|
||||
mod reviews;
|
||||
pub mod timespan;
|
||||
|
||||
|
|
143
rslib/src/sched/new.rs
Normal file
143
rslib/src/sched/new.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
// 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,
|
||||
decks::DeckID,
|
||||
err::Result,
|
||||
notes::NoteID,
|
||||
search::SortMode,
|
||||
types::Usn,
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
impl Card {
|
||||
fn schedule_as_new(&mut self, position: u32) {
|
||||
self.remove_from_filtered_deck_before_reschedule();
|
||||
self.due = position as i32;
|
||||
self.ctype = CardType::New;
|
||||
self.queue = CardQueue::New;
|
||||
self.interval = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// If the card is new, change its position.
|
||||
fn set_new_position(&mut self, position: u32) {
|
||||
if self.queue != CardQueue::New || self.ctype != CardType::New {
|
||||
return;
|
||||
}
|
||||
self.due = position as i32;
|
||||
}
|
||||
}
|
||||
pub(crate) struct NewCardSorter {
|
||||
position: HashMap<NoteID, u32>,
|
||||
}
|
||||
|
||||
impl NewCardSorter {
|
||||
pub(crate) fn new(cards: &[Card], starting_from: u32, step: u32, random: bool) -> Self {
|
||||
let nids: HashSet<_> = cards.iter().map(|c| c.note_id).collect();
|
||||
let mut nids: Vec<_> = nids.into_iter().collect();
|
||||
if random {
|
||||
nids.shuffle(&mut rand::thread_rng());
|
||||
} else {
|
||||
nids.sort_unstable();
|
||||
}
|
||||
|
||||
NewCardSorter {
|
||||
position: nids
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, nid)| (nid, ((i as u32) * step) + starting_from))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn position(&self, card: &Card) -> u32 {
|
||||
self.position
|
||||
.get(&card.note_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn reschedule_cards_as_new(&mut self, cids: &[CardID]) -> Result<()> {
|
||||
let usn = self.usn()?;
|
||||
let mut position = self.get_next_card_position();
|
||||
self.transact(None, |col| {
|
||||
col.set_search_table_to_card_ids(cids)?;
|
||||
let cards = col.storage.all_searched_cards()?;
|
||||
for mut card in cards {
|
||||
let original = card.clone();
|
||||
col.log_manually_scheduled_review(&card, usn, 0)?;
|
||||
card.schedule_as_new(position);
|
||||
col.update_card(&mut card, &original, usn)?;
|
||||
position += 1;
|
||||
}
|
||||
col.set_next_card_position(position)?;
|
||||
col.storage.clear_searched_cards_table()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sort_cards(
|
||||
&mut self,
|
||||
cids: &[CardID],
|
||||
starting_from: u32,
|
||||
step: u32,
|
||||
random: bool,
|
||||
shift: bool,
|
||||
) -> Result<()> {
|
||||
let usn = self.usn()?;
|
||||
self.transact(None, |col| {
|
||||
col.sort_cards_inner(cids, starting_from, step, random, shift, usn)
|
||||
})
|
||||
}
|
||||
|
||||
fn sort_cards_inner(
|
||||
&mut self,
|
||||
cids: &[CardID],
|
||||
starting_from: u32,
|
||||
step: u32,
|
||||
random: bool,
|
||||
shift: bool,
|
||||
usn: Usn,
|
||||
) -> Result<()> {
|
||||
if shift {
|
||||
self.shift_existing_cards(starting_from, step * cids.len() as u32, usn)?;
|
||||
}
|
||||
self.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 {
|
||||
let original = card.clone();
|
||||
card.set_new_position(sorter.position(&card));
|
||||
self.update_card(&mut card, &original, usn)?;
|
||||
}
|
||||
self.storage.clear_searched_cards_table()
|
||||
}
|
||||
|
||||
/// This creates a transaction - we probably want to split it out
|
||||
/// in the future if calling it as part of a deck options update.
|
||||
pub fn sort_deck(&mut self, deck: DeckID, random: bool) -> Result<()> {
|
||||
let cids = self.search_cards(&format!("did:{}", deck), SortMode::NoOrder)?;
|
||||
self.sort_cards(&cids, 1, 1, random, false)
|
||||
}
|
||||
|
||||
fn shift_existing_cards(&mut self, start: u32, by: u32, usn: Usn) -> Result<()> {
|
||||
self.storage.search_cards_at_or_above_position(start)?;
|
||||
for mut card in self.storage.all_searched_cards()? {
|
||||
let original = card.clone();
|
||||
card.set_new_position(card.due as u32 + by);
|
||||
self.update_card(&mut card, &original, usn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@ impl Collection {
|
|||
card.schedule_as_review(interval, today);
|
||||
col.update_card(&mut card, &original, usn)?;
|
||||
}
|
||||
col.clear_searched_cards()?;
|
||||
col.storage.clear_searched_cards_table()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ use crate::{
|
|||
card::CardID, card::CardType, collection::Collection, config::SortKind, err::Result,
|
||||
search::parser::parse,
|
||||
};
|
||||
use rusqlite::NO_PARAMS;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum SortMode {
|
||||
|
@ -99,9 +98,7 @@ impl Collection {
|
|||
let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?;
|
||||
self.add_order(&mut sql, mode)?;
|
||||
|
||||
self.storage
|
||||
.db
|
||||
.execute_batch(include_str!("search_cids_setup.sql"))?;
|
||||
self.storage.setup_searched_cards_table()?;
|
||||
let sql = format!("insert into search_cids {}", sql);
|
||||
|
||||
self.storage.db.prepare(&sql)?.execute(&args)?;
|
||||
|
@ -113,9 +110,7 @@ impl Collection {
|
|||
/// 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
|
||||
.db
|
||||
.execute_batch(include_str!("search_cids_setup.sql"))?;
|
||||
self.storage.setup_searched_cards_table()?;
|
||||
let mut stmt = self
|
||||
.storage
|
||||
.db
|
||||
|
@ -127,13 +122,6 @@ impl Collection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_searched_cards(&self) -> Result<()> {
|
||||
self.storage
|
||||
.db
|
||||
.execute("drop table if exists search_cids", NO_PARAMS)?;
|
||||
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 {
|
||||
|
|
|
@ -33,7 +33,7 @@ impl Collection {
|
|||
.get_revlog_entries_for_searched_cards(revlog_start)?
|
||||
};
|
||||
|
||||
self.clear_searched_cards()?;
|
||||
self.storage.clear_searched_cards_table()?;
|
||||
|
||||
Ok(pb::GraphsOut {
|
||||
cards: cards.into_iter().map(Into::into).collect(),
|
||||
|
|
5
rslib/src/storage/card/at_or_above_position.sql
Normal file
5
rslib/src/storage/card/at_or_above_position.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
insert into search_cids
|
||||
select id
|
||||
from cards
|
||||
where due >= ?
|
||||
and type = ?
|
|
@ -315,6 +315,26 @@ impl super::SqliteStorage {
|
|||
.next()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn search_cards_at_or_above_position(&self, start: u32) -> Result<()> {
|
||||
self.setup_searched_cards_table()?;
|
||||
self.db
|
||||
.prepare(include_str!("at_or_above_position.sql"))?
|
||||
.execute(&[start, CardType::New as u32])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn setup_searched_cards_table(&self) -> Result<()> {
|
||||
self.db
|
||||
.execute_batch(include_str!("search_cids_setup.sql"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_searched_cards_table(&self) -> Result<()> {
|
||||
self.db
|
||||
.execute("drop table if exists search_cids", NO_PARAMS)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
Loading…
Reference in a new issue