update scheduling ops

- migrate to CollectionOp()
- return actual change count when suspending/burying
- add helper to convert vec to vec of newtype
This commit is contained in:
Damien Elmes 2021-04-06 16:38:42 +10:00
parent 2de8cc1a94
commit 84fe309583
15 changed files with 172 additions and 127 deletions

View file

@ -16,7 +16,7 @@ from typing import List, Optional, Sequence
from anki.cards import CardId
from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV
from anki.decks import DeckConfigDict, DeckId, DeckTreeNode
from anki.notes import Note
from anki.notes import NoteId
from anki.utils import ids2str, intTime
CongratsInfo = _pb.CongratsInfoOut
@ -123,20 +123,31 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
) -> None:
self.col._backend.unbury_cards_in_current_deck(mode)
def suspend_cards(self, ids: Sequence[CardId]) -> OpChanges:
def suspend_cards(self, ids: Sequence[CardId]) -> OpChangesWithCount:
return self.col._backend.bury_or_suspend_cards(
card_ids=ids, mode=BuryOrSuspend.SUSPEND
card_ids=ids, note_ids=[], mode=BuryOrSuspend.SUSPEND
)
def bury_cards(self, ids: Sequence[CardId], manual: bool = True) -> OpChanges:
def suspend_notes(self, ids: Sequence[NoteId]) -> OpChangesWithCount:
return self.col._backend.bury_or_suspend_cards(
card_ids=[], note_ids=ids, mode=BuryOrSuspend.SUSPEND
)
def bury_cards(
self, ids: Sequence[CardId], manual: bool = True
) -> OpChangesWithCount:
if manual:
mode = BuryOrSuspend.BURY_USER
else:
mode = BuryOrSuspend.BURY_SCHED
return self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
return self.col._backend.bury_or_suspend_cards(
card_ids=ids, note_ids=[], mode=mode
)
def bury_note(self, note: Note) -> None:
self.bury_cards(note.card_ids())
def bury_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount:
return self.col._backend.bury_or_suspend_cards(
card_ids=[], note_ids=note_ids, mode=BuryOrSuspend.BURY_USER
)
# Resetting/rescheduling
##########################################################################

View file

@ -501,7 +501,7 @@ def test_misc():
col.addNote(note)
c = note.cards()[0]
# burying
col.sched.bury_note(note)
col.sched.bury_notes([note.id])
col.reset()
assert not col.sched.getCard()
col.sched.unbury_cards_in_current_deck()

View file

@ -732,9 +732,9 @@ where id in %s"""
def suspend_selected_cards(self, checked: bool) -> None:
cids = self.selected_cards()
if checked:
suspend_cards(mw=self.mw, card_ids=cids)
suspend_cards(parent=self, card_ids=cids).run_in_background()
else:
unsuspend_cards(mw=self.mw, card_ids=cids)
unsuspend_cards(parent=self.mw, card_ids=cids).run_in_background()
# Exporting
######################################################################
@ -796,25 +796,23 @@ where id in %s"""
return
reposition_new_cards_dialog(
mw=self.mw, parent=self, card_ids=self.selected_cards()
)
parent=self, card_ids=self.selected_cards()
).run_in_background()
@ensure_editor_saved_on_trigger
def set_due_date(self) -> None:
set_due_date_dialog(
mw=self.mw,
parent=self,
card_ids=self.selected_cards(),
config_key=Config.String.SET_DUE_BROWSER,
)
).run_in_background()
@ensure_editor_saved_on_trigger
def forget_cards(self) -> None:
forget_cards(
mw=self.mw,
parent=self,
card_ids=self.selected_cards(),
)
).run_in_background()
# Edit: selection
######################################################################

View file

@ -310,7 +310,9 @@ class FilteredDeckConfigDialog(QDialog):
gui_hooks.filtered_deck_dialog_will_add_or_update_deck(self, self.deck)
add_or_update_filtered_deck(mw=self.mw, deck=self.deck, success=success)
add_or_update_filtered_deck(parent=self, deck=self.deck).success(
success
).run_in_background()
# Step load/save
########################################################

View file

@ -7,28 +7,32 @@ from typing import Optional, Sequence
import aqt
from anki.cards import CardId
from anki.collection import CARD_TYPE_NEW, Config
from anki.collection import (
CARD_TYPE_NEW,
Config,
OpChanges,
OpChangesWithCount,
OpChangesWithId,
)
from anki.decks import DeckId
from anki.notes import NoteId
from anki.scheduler import FilteredDeckForUpdate
from aqt import AnkiQt
from aqt.main import PerformOpOptionalSuccessCallback
from aqt.operations import CollectionOp
from aqt.qt import *
from aqt.utils import disable_help_button, getText, tooltip, tr
def set_due_date_dialog(
*,
mw: aqt.AnkiQt,
parent: QWidget,
card_ids: Sequence[CardId],
config_key: Optional[Config.String.Key.V],
) -> None:
) -> Optional[CollectionOp[OpChanges]]:
if not card_ids:
return
return None
default_text = (
mw.col.get_config_string(config_key) if config_key is not None else ""
aqt.mw.col.get_config_string(config_key) if config_key is not None else ""
)
prompt = "\n".join(
[
@ -43,34 +47,35 @@ def set_due_date_dialog(
title=tr.actions_set_due_date(),
)
if not success or not days.strip():
return
mw.perform_op(
lambda: mw.col.sched.set_due_date(card_ids, days, config_key),
success=lambda _: tooltip(
tr.scheduling_set_due_date_done(cards=len(card_ids)),
parent=parent,
),
)
return None
else:
return CollectionOp(
parent, lambda col: col.sched.set_due_date(card_ids, days, config_key)
).success(
lambda _: tooltip(
tr.scheduling_set_due_date_done(cards=len(card_ids)),
parent=parent,
)
)
def forget_cards(
*, mw: aqt.AnkiQt, parent: QWidget, card_ids: Sequence[CardId]
) -> None:
if not card_ids:
return
mw.perform_op(
lambda: mw.col.sched.schedule_cards_as_new(card_ids),
success=lambda _: tooltip(
*, parent: QWidget, card_ids: Sequence[CardId]
) -> CollectionOp[OpChanges]:
return CollectionOp(
parent, lambda col: col.sched.schedule_cards_as_new(card_ids)
).success(
lambda _: tooltip(
tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent
),
)
)
def reposition_new_cards_dialog(
*, mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardId]
) -> None:
*, parent: QWidget, card_ids: Sequence[CardId]
) -> Optional[CollectionOp[OpChangesWithCount]]:
from aqt import mw
assert mw.col.db
row = mw.col.db.first(
f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0"
@ -92,15 +97,14 @@ def reposition_new_cards_dialog(
frm.start.selectAll()
if not d.exec_():
return
return None
start = frm.start.value()
step = frm.step.value()
randomize = frm.randomize.isChecked()
shift = frm.shift.isChecked()
reposition_new_cards(
mw=mw,
return reposition_new_cards(
parent=parent,
card_ids=card_ids,
starting_from=start,
@ -112,89 +116,80 @@ def reposition_new_cards_dialog(
def reposition_new_cards(
*,
mw: AnkiQt,
parent: QWidget,
card_ids: Sequence[CardId],
starting_from: int,
step_size: int,
randomize: bool,
shift_existing: bool,
) -> None:
mw.perform_op(
lambda: mw.col.sched.reposition_new_cards(
) -> CollectionOp[OpChangesWithCount]:
return CollectionOp(
parent,
lambda col: col.sched.reposition_new_cards(
card_ids=card_ids,
starting_from=starting_from,
step_size=step_size,
randomize=randomize,
shift_existing=shift_existing,
),
success=lambda out: tooltip(
).success(
lambda out: tooltip(
tr.browsing_changed_new_position(count=out.count), parent=parent
),
)
)
def suspend_cards(
*,
mw: AnkiQt,
parent: QWidget,
card_ids: Sequence[CardId],
success: PerformOpOptionalSuccessCallback = None,
) -> None:
mw.perform_op(lambda: mw.col.sched.suspend_cards(card_ids), success=success)
) -> CollectionOp[OpChangesWithCount]:
return CollectionOp(parent, lambda col: col.sched.suspend_cards(card_ids))
def suspend_note(
*,
mw: AnkiQt,
note_id: NoteId,
success: PerformOpOptionalSuccessCallback = None,
) -> None:
mw.taskman.run_in_background(
lambda: mw.col.card_ids_of_note(note_id),
lambda future: suspend_cards(mw=mw, card_ids=future.result(), success=success),
)
parent: QWidget,
note_ids: Sequence[NoteId],
) -> CollectionOp[OpChangesWithCount]:
return CollectionOp(parent, lambda col: col.sched.suspend_notes(note_ids))
def unsuspend_cards(*, mw: AnkiQt, card_ids: Sequence[CardId]) -> None:
mw.perform_op(lambda: mw.col.sched.unsuspend_cards(card_ids))
def unsuspend_cards(
*, parent: QWidget, card_ids: Sequence[CardId]
) -> CollectionOp[OpChanges]:
return CollectionOp(parent, lambda col: col.sched.unsuspend_cards(card_ids))
def bury_cards(
*,
mw: AnkiQt,
parent: QWidget,
card_ids: Sequence[CardId],
success: PerformOpOptionalSuccessCallback = None,
) -> None:
mw.perform_op(lambda: mw.col.sched.bury_cards(card_ids), success=success)
) -> CollectionOp[OpChangesWithCount]:
return CollectionOp(parent, lambda col: col.sched.bury_cards(card_ids))
def bury_note(
def bury_notes(
*,
mw: AnkiQt,
note_id: NoteId,
success: PerformOpOptionalSuccessCallback = None,
) -> None:
mw.taskman.run_in_background(
lambda: mw.col.card_ids_of_note(note_id),
lambda future: bury_cards(mw=mw, card_ids=future.result(), success=success),
)
parent: QWidget,
note_ids: Sequence[NoteId],
) -> CollectionOp[OpChangesWithCount]:
return CollectionOp(parent, lambda col: col.sched.bury_notes(note_ids))
def rebuild_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None:
mw.perform_op(lambda: mw.col.sched.rebuild_filtered_deck(deck_id))
def rebuild_filtered_deck(
*, parent: QWidget, deck_id: DeckId
) -> CollectionOp[OpChangesWithCount]:
return CollectionOp(parent, lambda col: col.sched.rebuild_filtered_deck(deck_id))
def empty_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None:
mw.perform_op(lambda: mw.col.sched.empty_filtered_deck(deck_id))
def empty_filtered_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]:
return CollectionOp(parent, lambda col: col.sched.empty_filtered_deck(deck_id))
def add_or_update_filtered_deck(
*,
mw: AnkiQt,
parent: QWidget,
deck: FilteredDeckForUpdate,
success: PerformOpOptionalSuccessCallback,
) -> None:
mw.perform_op(
lambda: mw.col.sched.add_or_update_filtered_deck(deck),
success=success,
)
) -> CollectionOp[OpChangesWithId]:
return CollectionOp(parent, lambda col: col.sched.add_or_update_filtered_deck(deck))

View file

@ -119,12 +119,14 @@ class Overview:
return self.mw.col.decks.current()["dyn"]
def rebuild_current_filtered_deck(self) -> None:
if self._current_deck_is_filtered():
rebuild_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected())
rebuild_filtered_deck(
parent=self.mw, deck_id=self.mw.col.decks.selected()
).run_in_background()
def empty_current_filtered_deck(self) -> None:
if self._current_deck_is_filtered():
empty_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected())
empty_filtered_deck(
parent=self.mw, deck_id=self.mw.col.decks.selected()
).run_in_background()
def onCustomStudyKey(self) -> None:
if not self._current_deck_is_filtered():

View file

@ -22,7 +22,7 @@ from aqt.operations.card import set_card_flag
from aqt.operations.note import remove_notes
from aqt.operations.scheduling import (
bury_cards,
bury_note,
bury_notes,
set_due_date_dialog,
suspend_cards,
suspend_note,
@ -861,39 +861,34 @@ time = %(time)d;
return
set_due_date_dialog(
mw=self.mw,
parent=self.mw,
card_ids=[self.card.id],
config_key=Config.String.SET_DUE_REVIEWER,
)
).run_in_background()
def suspend_current_note(self) -> None:
suspend_note(
mw=self.mw,
note_id=self.card.nid,
success=lambda _: tooltip(tr.studying_note_suspended()),
)
parent=self.mw,
note_ids=[self.card.nid],
).success(lambda _: tooltip(tr.studying_note_suspended())).run_in_background()
def suspend_current_card(self) -> None:
suspend_cards(
mw=self.mw,
parent=self.mw,
card_ids=[self.card.id],
success=lambda _: tooltip(tr.studying_card_suspended()),
)
).success(lambda _: tooltip(tr.studying_card_suspended())).run_in_background()
def bury_current_note(self) -> None:
bury_note(
mw=self.mw,
note_id=self.card.nid,
success=lambda _: tooltip(tr.studying_note_buried()),
)
bury_notes(
parent=self.mw,
note_ids=[self.card.nid],
).success(lambda _: tooltip(tr.studying_note_buried())).run_in_background()
def bury_current_card(self) -> None:
bury_cards(
mw=self.mw,
parent=self.mw,
card_ids=[self.card.id],
success=lambda _: tooltip(tr.studying_card_buried()),
)
).success(lambda _: tooltip(tr.studying_card_buried())).run_in_background()
def delete_current_note(self) -> None:
# need to check state because the shortcut is global to the main

View file

@ -120,7 +120,7 @@ service SchedulingService {
rpc CongratsInfo(Empty) returns (CongratsInfoOut);
rpc RestoreBuriedAndSuspendedCards(CardIds) returns (OpChanges);
rpc UnburyCardsInCurrentDeck(UnburyCardsInCurrentDeckIn) returns (Empty);
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChanges);
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChangesWithCount);
rpc EmptyFilteredDeck(DeckId) returns (OpChanges);
rpc RebuildFilteredDeck(DeckId) returns (OpChangesWithCount);
rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (OpChanges);
@ -1314,7 +1314,8 @@ message BuryOrSuspendCardsIn {
BURY_USER = 2;
}
repeated int64 card_ids = 1;
Mode mode = 2;
repeated int64 note_ids = 2;
Mode mode = 3;
}
message ScheduleCardsAsNewIn {

View file

@ -131,7 +131,7 @@ impl NotesService for Backend {
fn cards_of_note(&self, input: pb::NoteId) -> Result<pb::CardIds> {
self.with_col(|col| {
col.storage
.all_card_ids_of_note(NoteId(input.nid))
.all_card_ids_of_note_in_order(NoteId(input.nid))
.map(|v| pb::CardIds {
cids: v.into_iter().map(Into::into).collect(),
})

View file

@ -87,10 +87,18 @@ impl SchedulingService for Backend {
})
}
fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> Result<pb::OpChanges> {
fn bury_or_suspend_cards(
&self,
input: pb::BuryOrSuspendCardsIn,
) -> Result<pb::OpChangesWithCount> {
self.with_col(|col| {
let mode = input.mode();
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
let cids = if input.card_ids.is_empty() {
col.storage
.card_ids_of_notes(&input.note_ids.into_newtype(NoteId))?
} else {
input.card_ids.into_newtype(CardId)
};
col.bury_or_suspend_cards(&cids, mode).map(Into::into)
})
}
@ -105,7 +113,7 @@ impl SchedulingService for Backend {
fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> Result<pb::OpChanges> {
self.with_col(|col| {
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
let cids = input.card_ids.into_newtype(CardId);
let log = input.log;
col.reschedule_cards_as_new(&cids, log).map(Into::into)
})
@ -114,12 +122,12 @@ impl SchedulingService for Backend {
fn set_due_date(&self, input: pb::SetDueDateIn) -> Result<pb::OpChanges> {
let config = input.config_key.map(Into::into);
let days = input.days;
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
let cids = input.card_ids.into_newtype(CardId);
self.with_col(|col| col.set_due_date(&cids, &days, config).map(Into::into))
}
fn sort_cards(&self, input: pb::SortCardsIn) -> Result<pb::OpChangesWithCount> {
let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect();
let cids = input.card_ids.into_newtype(CardId);
let (start, step, random, shift) = (
input.starting_from,
input.step_size,

View file

@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub(crate) use crate::types::IntoNewtypeVec;
pub use crate::{
card::{Card, CardId},
collection::Collection,

View file

@ -33,7 +33,7 @@ mod test {
let queued = col.next_card()?.unwrap();
let nid = note.id;
let cid = queued.card.id;
let sibling_cid = col.storage.all_card_ids_of_note(nid)?[1];
let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1];
let assert_initial_state = |col: &mut Collection| -> Result<()> {
let first = col.storage.get_card(cid)?.unwrap();

View file

@ -89,7 +89,8 @@ impl Collection {
/// Bury/suspend cards in search table, and clear it.
/// Marks the cards as modified.
fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result<()> {
fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result<usize> {
let mut count = 0;
let usn = self.usn()?;
let sched = self.scheduler_version();
@ -113,18 +114,21 @@ impl Collection {
card.remove_from_learning();
}
card.queue = desired_queue;
count += 1;
self.update_card_inner(&mut card, original, usn)?;
}
}
self.storage.clear_searched_cards_table()
self.storage.clear_searched_cards_table()?;
Ok(count)
}
pub fn bury_or_suspend_cards(
&mut self,
cids: &[CardId],
mode: BuryOrSuspendMode,
) -> Result<OpOutput<()>> {
) -> Result<OpOutput<usize>> {
let op = match mode {
BuryOrSuspendMode::Suspend => Op::Suspend,
BuryOrSuspendMode::BurySched | BuryOrSuspendMode::BuryUser => Op::Bury,
@ -141,7 +145,7 @@ impl Collection {
nid: NoteId,
include_new: bool,
include_reviews: bool,
) -> Result<()> {
) -> Result<usize> {
self.storage
.search_siblings_for_bury(cid, nid, include_new, include_reviews)?;
self.bury_or_suspend_searched_cards(BuryOrSuspendMode::BurySched)

View file

@ -308,13 +308,26 @@ impl super::SqliteStorage {
.collect()
}
pub(crate) fn all_card_ids_of_note(&self, nid: NoteId) -> Result<Vec<CardId>> {
pub(crate) fn all_card_ids_of_note_in_order(&self, nid: NoteId) -> Result<Vec<CardId>> {
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 card_ids_of_notes(&self, nids: &[NoteId]) -> Result<Vec<CardId>> {
let mut stmt = self
.db
.prepare_cached("select id from cards where nid = ?")?;
let mut cids = vec![];
for nid in nids {
for cid in stmt.query_map(&[nid], |row| row.get(0))? {
cids.push(cid?);
}
}
Ok(cids)
}
/// Place matching card ids into the search table.
pub(crate) fn search_siblings_for_bury(
&self,

View file

@ -68,3 +68,18 @@ macro_rules! define_newtype {
}
define_newtype!(Usn, i32);
pub(crate) trait IntoNewtypeVec {
fn into_newtype<F, T>(self, func: F) -> Vec<T>
where
F: FnMut(i64) -> T;
}
impl IntoNewtypeVec for Vec<i64> {
fn into_newtype<F, T>(self, func: F) -> Vec<T>
where
F: FnMut(i64) -> T,
{
self.into_iter().map(func).collect()
}
}