mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00
add ability to force interval reset
- use trailing ! to force a reset - use - instead of .. - tweak i18n messages and error handling
This commit is contained in:
parent
b9635ce936
commit
a8ddb65e1c
8 changed files with 118 additions and 55 deletions
3
ftl/core/errors.ftl
Normal file
3
ftl/core/errors.ftl
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
errors-invalid-input-empty = Invalid input.
|
||||||
|
errors-invalid-input-details = Invalid input: { $details }
|
||||||
|
errors-parse-number-fail = A number was invalid or out of range.
|
|
@ -146,16 +146,19 @@ scheduling-deck-updated =
|
||||||
scheduling-set-due-date-prompt =
|
scheduling-set-due-date-prompt =
|
||||||
{ $cards ->
|
{ $cards ->
|
||||||
[one] Show card in how many days?
|
[one] Show card in how many days?
|
||||||
*[other] Show cards in how many days? (eg 1, or 1..7)
|
*[other] Show cards in how many days?
|
||||||
}
|
}
|
||||||
scheduling-set-due-date-changed-cards =
|
scheduling-set-due-date-prompt-hint =
|
||||||
|
0 = today
|
||||||
|
1! = tomorrow+reset review interval
|
||||||
|
3-7 = random choice of 3-7 days
|
||||||
|
scheduling-set-due-date-done =
|
||||||
{ $cards ->
|
{ $cards ->
|
||||||
[one] Changed card's due date.
|
[one] Set due date of { $cards } card.
|
||||||
*[other] Changed due date of { $cards } cards.
|
*[other] Set due date of { $cards } cards.
|
||||||
}
|
}
|
||||||
scheduling-set-due-date-invalid-input = Expected a number or range (eg 1, or 1..7)
|
|
||||||
scheduling-forgot-cards =
|
scheduling-forgot-cards =
|
||||||
{ $cards ->
|
{ $cards ->
|
||||||
[one] { $cards } card placed at the end of the new card queue.
|
[one] Forgot { $card } card.
|
||||||
*[other] { $cards } cards placed at the end of the new card queue.
|
*[other] Forgot { $cards } cards.
|
||||||
}
|
}
|
||||||
|
|
|
@ -1430,7 +1430,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
||||||
def reschedCards(
|
def reschedCards(
|
||||||
self, card_ids: List[int], min_interval: int, max_interval: int
|
self, card_ids: List[int], min_interval: int, max_interval: int
|
||||||
) -> None:
|
) -> None:
|
||||||
self.set_due_date(card_ids, f"{min_interval}..{max_interval}")
|
self.set_due_date(card_ids, f"{min_interval}-{max_interval}!")
|
||||||
|
|
||||||
forgetCards = schedule_cards_as_new
|
forgetCards = schedule_cards_as_new
|
||||||
|
|
||||||
|
|
|
@ -1124,13 +1124,7 @@ def test_resched():
|
||||||
assert c.due == col.sched.today
|
assert c.due == col.sched.today
|
||||||
assert c.ivl == 1
|
assert c.ivl == 1
|
||||||
assert c.queue == QUEUE_TYPE_REV and c.type == CARD_TYPE_REV
|
assert c.queue == QUEUE_TYPE_REV and c.type == CARD_TYPE_REV
|
||||||
# make it due tomorrow, which increases its interval by a day
|
# make it due tomorrow
|
||||||
col.sched.reschedCards([c.id], 1, 1)
|
|
||||||
c.load()
|
|
||||||
assert c.due == col.sched.today + 1
|
|
||||||
assert c.ivl == 2
|
|
||||||
# but if it was new, that would not happen
|
|
||||||
col.sched.forgetCards([c.id])
|
|
||||||
col.sched.reschedCards([c.id], 1, 1)
|
col.sched.reschedCards([c.id], 1, 1)
|
||||||
c.load()
|
c.load()
|
||||||
assert c.due == col.sched.today + 1
|
assert c.due == col.sched.today + 1
|
||||||
|
|
|
@ -8,7 +8,6 @@ from typing import List
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import Config
|
from anki.collection import Config
|
||||||
from anki.errors import InvalidInput
|
|
||||||
from anki.lang import TR
|
from anki.lang import TR
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import getText, showWarning, tooltip, tr
|
from aqt.utils import getText, showWarning, tooltip, tr
|
||||||
|
@ -26,9 +25,15 @@ def set_due_date_dialog(
|
||||||
return
|
return
|
||||||
|
|
||||||
default = mw.col.get_config_string(default_key)
|
default = mw.col.get_config_string(default_key)
|
||||||
|
prompt = "\n".join(
|
||||||
|
[
|
||||||
|
tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)),
|
||||||
|
tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT_HINT),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
(days, success) = getText(
|
(days, success) = getText(
|
||||||
prompt=tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)),
|
prompt=prompt,
|
||||||
parent=parent,
|
parent=parent,
|
||||||
default=default,
|
default=default,
|
||||||
title=tr(TR.ACTIONS_SET_DUE_DATE),
|
title=tr(TR.ACTIONS_SET_DUE_DATE),
|
||||||
|
@ -45,16 +50,12 @@ def set_due_date_dialog(
|
||||||
try:
|
try:
|
||||||
fut.result()
|
fut.result()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, InvalidInput):
|
showWarning(str(e))
|
||||||
err = tr(TR.SCHEDULING_SET_DUE_DATE_INVALID_INPUT)
|
|
||||||
else:
|
|
||||||
err = str(e)
|
|
||||||
showWarning(err)
|
|
||||||
on_done()
|
on_done()
|
||||||
return
|
return
|
||||||
|
|
||||||
tooltip(
|
tooltip(
|
||||||
tr(TR.SCHEDULING_SET_DUE_DATE_CHANGED_CARDS, cards=len(card_ids)),
|
tr(TR.SCHEDULING_SET_DUE_DATE_DONE, cards=len(card_ids)),
|
||||||
parent=parent,
|
parent=parent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -676,8 +676,8 @@ impl BackendService for Backend {
|
||||||
|
|
||||||
fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult<pb::Empty> {
|
fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult<pb::Empty> {
|
||||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
||||||
let (min, max) = parse_due_date_str(&input.days)?;
|
let spec = parse_due_date_str(&input.days)?;
|
||||||
self.with_col(|col| col.set_due_date(&cids, min, max).map(Into::into))
|
self.with_col(|col| col.set_due_date(&cids, spec).map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult<Empty> {
|
fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult<Empty> {
|
||||||
|
|
|
@ -197,6 +197,17 @@ impl AnkiError {
|
||||||
tr_args!("reason" => reason.into_owned()),
|
tr_args!("reason" => reason.into_owned()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AnkiError::InvalidInput { info } => {
|
||||||
|
if info.is_empty() {
|
||||||
|
i18n.tr(TR::ErrorsInvalidInputEmpty).into()
|
||||||
|
} else {
|
||||||
|
i18n.trn(
|
||||||
|
TR::ErrorsInvalidInputDetails,
|
||||||
|
tr_args!("details" => info.to_owned()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnkiError::ParseNumError => i18n.tr(TR::ErrorsParseNumberFail).into(),
|
||||||
_ => format!("{:?}", self),
|
_ => format!("{:?}", self),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,11 @@ impl Card {
|
||||||
/// Relearning cards have their interval preserved. Normal review
|
/// Relearning cards have their interval preserved. Normal review
|
||||||
/// cards have their interval adjusted based on change between the
|
/// cards have their interval adjusted based on change between the
|
||||||
/// previous and new due date.
|
/// previous and new due date.
|
||||||
fn set_due_date(&mut self, today: u32, days_from_today: u32) {
|
fn set_due_date(&mut self, today: u32, days_from_today: u32, force_reset: bool) {
|
||||||
let new_due = (today + days_from_today) as i32;
|
let new_due = (today + days_from_today) as i32;
|
||||||
let new_interval = if let Some(old_due) = self.current_review_due_day() {
|
let new_interval = if force_reset {
|
||||||
|
days_from_today
|
||||||
|
} else if let Some(old_due) = self.current_review_due_day() {
|
||||||
// review cards have their interval shifted based on actual elapsed time
|
// review cards have their interval shifted based on actual elapsed time
|
||||||
let days_early = old_due - new_due;
|
let days_early = old_due - new_due;
|
||||||
((self.interval as i32) - days_early).max(0) as u32
|
((self.interval as i32) - days_early).max(0) as u32
|
||||||
|
@ -62,44 +64,58 @@ impl Card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a number or range (eg '4' or '4..7') into min and max.
|
#[derive(Debug, PartialEq)]
|
||||||
pub fn parse_due_date_str(s: &str) -> Result<(u32, u32)> {
|
pub struct DueDateSpecifier {
|
||||||
|
min: u32,
|
||||||
|
max: u32,
|
||||||
|
force_reset: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_due_date_str(s: &str) -> Result<DueDateSpecifier> {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref SINGLE: Regex = Regex::new(r#"^\d+$"#).unwrap();
|
static ref RE: Regex = Regex::new(
|
||||||
static ref RANGE: Regex = Regex::new(
|
|
||||||
r#"(?x)^
|
r#"(?x)^
|
||||||
(\d+)
|
# a number
|
||||||
\.\.
|
(?P<min>\d+)
|
||||||
(\d+)
|
# an optional hypen and another number
|
||||||
|
(?:
|
||||||
|
-
|
||||||
|
(?P<max>\d+)
|
||||||
|
)?
|
||||||
|
# optional exclamation mark
|
||||||
|
(?P<bang>!)?
|
||||||
$
|
$
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
if SINGLE.is_match(s) {
|
let caps = RE.captures(s).ok_or_else(|| AnkiError::invalid_input(s))?;
|
||||||
let num: u32 = s.parse()?;
|
let min: u32 = caps.name("min").unwrap().as_str().parse()?;
|
||||||
Ok((num, num))
|
let max = if let Some(max) = caps.name("max") {
|
||||||
} else if let Some(cap) = RANGE.captures_iter(s).next() {
|
max.as_str().parse()?
|
||||||
let one: u32 = cap[1].parse()?;
|
|
||||||
let two: u32 = cap[2].parse()?;
|
|
||||||
Ok((one.min(two), two.max(one)))
|
|
||||||
} else {
|
} else {
|
||||||
Err(AnkiError::ParseNumError)
|
min
|
||||||
}
|
};
|
||||||
|
let force_reset = caps.name("bang").is_some();
|
||||||
|
Ok(DueDateSpecifier {
|
||||||
|
min: min.min(max),
|
||||||
|
max: max.max(min),
|
||||||
|
force_reset,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn set_due_date(&mut self, cids: &[CardID], min_days: u32, max_days: u32) -> Result<()> {
|
pub fn set_due_date(&mut self, cids: &[CardID], spec: DueDateSpecifier) -> Result<()> {
|
||||||
let usn = self.usn()?;
|
let usn = self.usn()?;
|
||||||
let today = self.timing_today()?.days_elapsed;
|
let today = self.timing_today()?.days_elapsed;
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let distribution = Uniform::from(min_days..=max_days);
|
let distribution = Uniform::from(spec.min..=spec.max);
|
||||||
self.transact(None, |col| {
|
self.transact(None, |col| {
|
||||||
col.storage.set_search_table_to_card_ids(cids, false)?;
|
col.storage.set_search_table_to_card_ids(cids, false)?;
|
||||||
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 days_from_today = distribution.sample(&mut rng);
|
let days_from_today = distribution.sample(&mut rng);
|
||||||
card.set_due_date(today, days_from_today);
|
card.set_due_date(today, days_from_today, spec.force_reset);
|
||||||
col.log_manually_scheduled_review(&card, &original, usn)?;
|
col.log_manually_scheduled_review(&card, &original, usn)?;
|
||||||
col.update_card(&mut card, &original, usn)?;
|
col.update_card(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
|
@ -116,12 +132,42 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse() -> Result<()> {
|
fn parse() -> Result<()> {
|
||||||
|
type S = DueDateSpecifier;
|
||||||
assert!(parse_due_date_str("").is_err());
|
assert!(parse_due_date_str("").is_err());
|
||||||
assert!(parse_due_date_str("x").is_err());
|
assert!(parse_due_date_str("x").is_err());
|
||||||
assert!(parse_due_date_str("-5").is_err());
|
assert!(parse_due_date_str("-5").is_err());
|
||||||
assert_eq!(parse_due_date_str("5")?, (5, 5));
|
assert_eq!(
|
||||||
assert_eq!(parse_due_date_str("50..70")?, (50, 70));
|
parse_due_date_str("5")?,
|
||||||
assert_eq!(parse_due_date_str("70..50")?, (50, 70));
|
S {
|
||||||
|
min: 5,
|
||||||
|
max: 5,
|
||||||
|
force_reset: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_due_date_str("5!")?,
|
||||||
|
S {
|
||||||
|
min: 5,
|
||||||
|
max: 5,
|
||||||
|
force_reset: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_due_date_str("50-70")?,
|
||||||
|
S {
|
||||||
|
min: 50,
|
||||||
|
max: 70,
|
||||||
|
force_reset: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_due_date_str("70-50!")?,
|
||||||
|
S {
|
||||||
|
min: 50,
|
||||||
|
max: 70,
|
||||||
|
force_reset: true
|
||||||
|
}
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,29 +176,34 @@ mod test {
|
||||||
let mut c = Card::new(NoteID(0), 0, DeckID(0), 0);
|
let mut c = Card::new(NoteID(0), 0, DeckID(0), 0);
|
||||||
|
|
||||||
// setting the due date of a new card will convert it
|
// setting the due date of a new card will convert it
|
||||||
c.set_due_date(5, 2);
|
c.set_due_date(5, 2, false);
|
||||||
assert_eq!(c.ctype, CardType::Review);
|
assert_eq!(c.ctype, CardType::Review);
|
||||||
assert_eq!(c.due, 7);
|
assert_eq!(c.due, 7);
|
||||||
assert_eq!(c.interval, 2);
|
assert_eq!(c.interval, 2);
|
||||||
|
|
||||||
// reschedule it again the next day, shifting it from day 7 to day 9
|
// reschedule it again the next day, shifting it from day 7 to day 9
|
||||||
c.set_due_date(6, 3);
|
c.set_due_date(6, 3, false);
|
||||||
assert_eq!(c.due, 9);
|
assert_eq!(c.due, 9);
|
||||||
// we moved it 2 days forward from its original 2 day interval, and the
|
// we moved it 2 days forward from its original 2 day interval, and the
|
||||||
// interval should match the new delay
|
// interval should match the new delay
|
||||||
assert_eq!(c.interval, 4);
|
assert_eq!(c.interval, 4);
|
||||||
|
|
||||||
// we can bring cards forward too - return it to its original due date
|
// we can bring cards forward too - return it to its original due date
|
||||||
c.set_due_date(6, 1);
|
c.set_due_date(6, 1, false);
|
||||||
assert_eq!(c.due, 7);
|
assert_eq!(c.due, 7);
|
||||||
assert_eq!(c.interval, 2);
|
assert_eq!(c.interval, 2);
|
||||||
|
|
||||||
|
// we can force the interval to be reset instead of shifted
|
||||||
|
c.set_due_date(6, 2, true);
|
||||||
|
assert_eq!(c.due, 8);
|
||||||
|
assert_eq!(c.interval, 2);
|
||||||
|
|
||||||
// should work in a filtered deck
|
// should work in a filtered deck
|
||||||
c.original_due = 7;
|
c.original_due = 7;
|
||||||
c.original_deck_id = DeckID(1);
|
c.original_deck_id = DeckID(1);
|
||||||
c.due = -10000;
|
c.due = -10000;
|
||||||
c.queue = CardQueue::New;
|
c.queue = CardQueue::New;
|
||||||
c.set_due_date(6, 1);
|
c.set_due_date(6, 1, false);
|
||||||
assert_eq!(c.due, 7);
|
assert_eq!(c.due, 7);
|
||||||
assert_eq!(c.interval, 2);
|
assert_eq!(c.interval, 2);
|
||||||
assert_eq!(c.queue, CardQueue::Review);
|
assert_eq!(c.queue, CardQueue::Review);
|
||||||
|
@ -163,7 +214,7 @@ mod test {
|
||||||
c.ctype = CardType::Relearn;
|
c.ctype = CardType::Relearn;
|
||||||
c.original_due = c.due;
|
c.original_due = c.due;
|
||||||
c.due = 12345678;
|
c.due = 12345678;
|
||||||
c.set_due_date(6, 10);
|
c.set_due_date(6, 10, false);
|
||||||
assert_eq!(c.due, 16);
|
assert_eq!(c.due, 16);
|
||||||
assert_eq!(c.interval, 10);
|
assert_eq!(c.interval, 10);
|
||||||
|
|
||||||
|
@ -171,7 +222,7 @@ mod test {
|
||||||
c.ctype = CardType::Relearn;
|
c.ctype = CardType::Relearn;
|
||||||
c.original_due = c.due;
|
c.original_due = c.due;
|
||||||
c.due = 12345678;
|
c.due = 12345678;
|
||||||
c.set_due_date(6, 1);
|
c.set_due_date(6, 1, false);
|
||||||
assert_eq!(c.due, 7);
|
assert_eq!(c.due, 7);
|
||||||
assert_eq!(c.interval, 10);
|
assert_eq!(c.interval, 10);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue