mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Create a new kind of revlog entry for reschedule cards on change (#3508)
* create a new kind of revlog entry for Reschedule cards on change * add comments * exclude the rescheduled case in reviews graph
This commit is contained in:
parent
30e734b925
commit
26ae51fafd
16 changed files with 76 additions and 24 deletions
|
@ -23,6 +23,7 @@ card-stats-review-log-type-review = Review
|
||||||
card-stats-review-log-type-relearn = Relearn
|
card-stats-review-log-type-relearn = Relearn
|
||||||
card-stats-review-log-type-filtered = Filtered
|
card-stats-review-log-type-filtered = Filtered
|
||||||
card-stats-review-log-type-manual = Manual
|
card-stats-review-log-type-manual = Manual
|
||||||
|
card-stats-review-log-type-rescheduled = Rescheduled
|
||||||
card-stats-review-log-elapsed-time = Elapsed Time
|
card-stats-review-log-elapsed-time = Elapsed Time
|
||||||
card-stats-no-card = (No card to display.)
|
card-stats-no-card = (No card to display.)
|
||||||
card-stats-custom-data = Custom Data
|
card-stats-custom-data = Custom Data
|
||||||
|
|
|
@ -207,6 +207,7 @@ message RevlogEntry {
|
||||||
RELEARNING = 2;
|
RELEARNING = 2;
|
||||||
FILTERED = 3;
|
FILTERED = 3;
|
||||||
MANUAL = 4;
|
MANUAL = 4;
|
||||||
|
RESCHEDULED = 5;
|
||||||
}
|
}
|
||||||
int64 id = 1;
|
int64 id = 1;
|
||||||
int64 cid = 2;
|
int64 cid = 2;
|
||||||
|
|
|
@ -72,6 +72,7 @@ pub enum RevlogReviewKind {
|
||||||
/// disabled.
|
/// disabled.
|
||||||
Filtered = 3,
|
Filtered = 3,
|
||||||
Manual = 4,
|
Manual = 4,
|
||||||
|
Rescheduled = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RevlogEntry {
|
impl RevlogEntry {
|
||||||
|
@ -86,11 +87,32 @@ impl RevlogEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
|
// set due date or reset
|
||||||
pub(crate) fn log_manually_scheduled_review(
|
pub(crate) fn log_manually_scheduled_review(
|
||||||
&mut self,
|
&mut self,
|
||||||
card: &Card,
|
card: &Card,
|
||||||
original_interval: u32,
|
original_interval: u32,
|
||||||
usn: Usn,
|
usn: Usn,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.log_scheduled_review(card, original_interval, usn, RevlogReviewKind::Manual)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reschedule cards on change
|
||||||
|
pub(crate) fn log_rescheduled_review(
|
||||||
|
&mut self,
|
||||||
|
card: &Card,
|
||||||
|
original_interval: u32,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.log_scheduled_review(card, original_interval, usn, RevlogReviewKind::Rescheduled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_scheduled_review(
|
||||||
|
&mut self,
|
||||||
|
card: &Card,
|
||||||
|
original_interval: u32,
|
||||||
|
usn: Usn,
|
||||||
|
review_kind: RevlogReviewKind,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let ease_factor = u32::from(
|
let ease_factor = u32::from(
|
||||||
card.memory_state
|
card.memory_state
|
||||||
|
@ -106,7 +128,7 @@ impl Collection {
|
||||||
last_interval: i32::try_from(original_interval).unwrap_or(i32::MAX),
|
last_interval: i32::try_from(original_interval).unwrap_or(i32::MAX),
|
||||||
ease_factor,
|
ease_factor,
|
||||||
taken_millis: 0,
|
taken_millis: 0,
|
||||||
review_kind: RevlogReviewKind::Manual,
|
review_kind,
|
||||||
};
|
};
|
||||||
self.add_revlog_entry_undoable(entry)?;
|
self.add_revlog_entry_undoable(entry)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -119,9 +119,10 @@ impl Collection {
|
||||||
};
|
};
|
||||||
*due = (timing.days_elapsed as i32) - days_elapsed
|
*due = (timing.days_elapsed as i32) - days_elapsed
|
||||||
+ card.interval as i32;
|
+ card.interval as i32;
|
||||||
// Add a manual revlog entry if the last entry wasn't manual
|
// Add a rescheduled revlog entry if the last entry wasn't
|
||||||
if !last_info.last_revlog_is_manual {
|
// rescheduled
|
||||||
self.log_manually_scheduled_review(
|
if !last_info.last_revlog_is_rescheduled {
|
||||||
|
self.log_rescheduled_review(
|
||||||
&card,
|
&card,
|
||||||
original_interval,
|
original_interval,
|
||||||
usn,
|
usn,
|
||||||
|
@ -237,7 +238,7 @@ struct LastRevlogInfo {
|
||||||
last_reviewed_at: Option<TimestampSecs>,
|
last_reviewed_at: Option<TimestampSecs>,
|
||||||
/// If true, the last action on this card was a reschedule, so we
|
/// If true, the last action on this card was a reschedule, so we
|
||||||
/// can avoid writing an extra revlog entry on another reschedule.
|
/// can avoid writing an extra revlog entry on another reschedule.
|
||||||
last_revlog_is_manual: bool,
|
last_revlog_is_rescheduled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a map of cards to info about last review/reschedule.
|
/// Return a map of cards to info about last review/reschedule.
|
||||||
|
@ -249,18 +250,18 @@ fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, LastRevlogIn
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|(card_id, group)| {
|
.for_each(|(card_id, group)| {
|
||||||
let mut last_reviewed_at = None;
|
let mut last_reviewed_at = None;
|
||||||
let mut last_revlog_is_manual = false;
|
let mut last_revlog_is_rescheduled = false;
|
||||||
for e in group.into_iter() {
|
for e in group.into_iter() {
|
||||||
if e.button_chosen >= 1 {
|
if e.button_chosen >= 1 {
|
||||||
last_reviewed_at = Some(e.id.as_secs());
|
last_reviewed_at = Some(e.id.as_secs());
|
||||||
}
|
}
|
||||||
last_revlog_is_manual = e.review_kind == RevlogReviewKind::Manual;
|
last_revlog_is_rescheduled = e.review_kind == RevlogReviewKind::Rescheduled;
|
||||||
}
|
}
|
||||||
out.insert(
|
out.insert(
|
||||||
card_id,
|
card_id,
|
||||||
LastRevlogInfo {
|
LastRevlogInfo {
|
||||||
last_reviewed_at,
|
last_reviewed_at,
|
||||||
last_revlog_is_manual,
|
last_revlog_is_rescheduled,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -321,7 +322,7 @@ pub(crate) fn single_card_revlog_to_item(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// only manual rescheduling; treat like empty
|
// only manual and rescheduled revlogs; treat like empty
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -84,7 +84,8 @@ impl From<crate::revlog::RevlogReviewKind> for fsrs::RevlogReviewKind {
|
||||||
crate::revlog::RevlogReviewKind::Review => fsrs::RevlogReviewKind::Review,
|
crate::revlog::RevlogReviewKind::Review => fsrs::RevlogReviewKind::Review,
|
||||||
crate::revlog::RevlogReviewKind::Relearning => fsrs::RevlogReviewKind::Relearning,
|
crate::revlog::RevlogReviewKind::Relearning => fsrs::RevlogReviewKind::Relearning,
|
||||||
crate::revlog::RevlogReviewKind::Filtered => fsrs::RevlogReviewKind::Filtered,
|
crate::revlog::RevlogReviewKind::Filtered => fsrs::RevlogReviewKind::Filtered,
|
||||||
crate::revlog::RevlogReviewKind::Manual => fsrs::RevlogReviewKind::Manual,
|
crate::revlog::RevlogReviewKind::Manual
|
||||||
|
| crate::revlog::RevlogReviewKind::Rescheduled => fsrs::RevlogReviewKind::Manual,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,6 +294,9 @@ pub(crate) fn single_card_revlog_to_items(
|
||||||
Some(RevlogEntry {
|
Some(RevlogEntry {
|
||||||
review_kind: RevlogReviewKind::Manual,
|
review_kind: RevlogReviewKind::Manual,
|
||||||
..
|
..
|
||||||
|
}) | Some(RevlogEntry {
|
||||||
|
review_kind: RevlogReviewKind::Rescheduled,
|
||||||
|
..
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -343,10 +346,12 @@ pub(crate) fn single_card_revlog_to_items(
|
||||||
// Filter out unwanted entries
|
// Filter out unwanted entries
|
||||||
entries.retain(|entry| {
|
entries.retain(|entry| {
|
||||||
!(
|
!(
|
||||||
// manually rescheduled
|
// set due date or reset
|
||||||
(entry.review_kind == RevlogReviewKind::Manual || entry.button_chosen == 0)
|
(entry.review_kind == RevlogReviewKind::Manual || entry.button_chosen == 0)
|
||||||
|| // cram
|
|| // cram
|
||||||
(entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0)
|
(entry.review_kind == RevlogReviewKind::Filtered && entry.ease_factor == 0)
|
||||||
|
|| // rescheduled
|
||||||
|
(entry.review_kind == RevlogReviewKind::Rescheduled)
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -407,6 +412,7 @@ fn revlog_entry_to_proto(e: RevlogEntry) -> anki_proto::stats::RevlogEntry {
|
||||||
RevlogReviewKind::Relearning => revlog_entry::ReviewKind::Relearning,
|
RevlogReviewKind::Relearning => revlog_entry::ReviewKind::Relearning,
|
||||||
RevlogReviewKind::Filtered => revlog_entry::ReviewKind::Filtered,
|
RevlogReviewKind::Filtered => revlog_entry::ReviewKind::Filtered,
|
||||||
RevlogReviewKind::Manual => revlog_entry::ReviewKind::Manual,
|
RevlogReviewKind::Manual => revlog_entry::ReviewKind::Manual,
|
||||||
|
RevlogReviewKind::Rescheduled => revlog_entry::ReviewKind::Rescheduled,
|
||||||
} as i32,
|
} as i32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ fn interval_bucket(review: &RevlogEntry) -> Option<IntervalBucket> {
|
||||||
} else {
|
} else {
|
||||||
IntervalBucket::Mature
|
IntervalBucket::Mature
|
||||||
}),
|
}),
|
||||||
RevlogReviewKind::Manual => None,
|
RevlogReviewKind::Manual | RevlogReviewKind::Rescheduled => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,9 @@ impl GraphsContext {
|
||||||
'outer: for review in &self.revlog {
|
'outer: for review in &self.revlog {
|
||||||
if matches!(
|
if matches!(
|
||||||
review.review_kind,
|
review.review_kind,
|
||||||
RevlogReviewKind::Filtered | RevlogReviewKind::Manual
|
RevlogReviewKind::Filtered
|
||||||
|
| RevlogReviewKind::Manual
|
||||||
|
| RevlogReviewKind::Rescheduled
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl GraphsContext {
|
||||||
self.revlog
|
self.revlog
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|review| {
|
.filter(|review| {
|
||||||
// not manually rescheduled
|
// not rescheduled/set due date/reset
|
||||||
review.button_chosen > 0
|
review.button_chosen > 0
|
||||||
// not cramming
|
// not cramming
|
||||||
&& (review.review_kind != RevlogReviewKind::Filtered || review.ease_factor != 0)
|
&& (review.review_kind != RevlogReviewKind::Filtered || review.ease_factor != 0)
|
||||||
|
|
|
@ -10,7 +10,9 @@ impl GraphsContext {
|
||||||
pub(super) fn review_counts_and_times(&self) -> ReviewCountsAndTimes {
|
pub(super) fn review_counts_and_times(&self) -> ReviewCountsAndTimes {
|
||||||
let mut data = ReviewCountsAndTimes::default();
|
let mut data = ReviewCountsAndTimes::default();
|
||||||
for review in &self.revlog {
|
for review in &self.revlog {
|
||||||
if review.review_kind == RevlogReviewKind::Manual {
|
if review.review_kind == RevlogReviewKind::Manual
|
||||||
|
|| review.review_kind == RevlogReviewKind::Rescheduled
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let day = (review.id.as_secs().elapsed_secs_since(self.next_day_start) / 86_400) as i32;
|
let day = (review.id.as_secs().elapsed_secs_since(self.next_day_start) / 86_400) as i32;
|
||||||
|
@ -38,7 +40,7 @@ impl GraphsContext {
|
||||||
count.filtered += 1;
|
count.filtered += 1;
|
||||||
time.filtered += review.taken_millis;
|
time.filtered += review.taken_millis;
|
||||||
}
|
}
|
||||||
RevlogReviewKind::Manual => unreachable!(),
|
RevlogReviewKind::Manual | RevlogReviewKind::Rescheduled => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data
|
data
|
||||||
|
|
|
@ -14,7 +14,9 @@ impl GraphsContext {
|
||||||
if review.id.0 < start_of_today_ms {
|
if review.id.0 < start_of_today_ms {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if review.review_kind == RevlogReviewKind::Manual {
|
if review.review_kind == RevlogReviewKind::Manual
|
||||||
|
|| review.review_kind == RevlogReviewKind::Rescheduled
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// total
|
// total
|
||||||
|
@ -37,7 +39,7 @@ impl GraphsContext {
|
||||||
RevlogReviewKind::Review => today.review_count += 1,
|
RevlogReviewKind::Review => today.review_count += 1,
|
||||||
RevlogReviewKind::Relearning => today.relearn_count += 1,
|
RevlogReviewKind::Relearning => today.relearn_count += 1,
|
||||||
RevlogReviewKind::Filtered => today.early_review_count += 1,
|
RevlogReviewKind::Filtered => today.early_review_count += 1,
|
||||||
RevlogReviewKind::Manual => unreachable!(),
|
RevlogReviewKind::Manual | RevlogReviewKind::Rescheduled => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
today
|
today
|
||||||
|
|
|
@ -46,6 +46,9 @@ impl From<RevlogReviewKind> for i32 {
|
||||||
RevlogReviewKind::Relearning => anki_proto::stats::revlog_entry::ReviewKind::Relearning,
|
RevlogReviewKind::Relearning => anki_proto::stats::revlog_entry::ReviewKind::Relearning,
|
||||||
RevlogReviewKind::Filtered => anki_proto::stats::revlog_entry::ReviewKind::Filtered,
|
RevlogReviewKind::Filtered => anki_proto::stats::revlog_entry::ReviewKind::Filtered,
|
||||||
RevlogReviewKind::Manual => anki_proto::stats::revlog_entry::ReviewKind::Manual,
|
RevlogReviewKind::Manual => anki_proto::stats::revlog_entry::ReviewKind::Manual,
|
||||||
|
RevlogReviewKind::Rescheduled => {
|
||||||
|
anki_proto::stats::revlog_entry::ReviewKind::Rescheduled
|
||||||
|
}
|
||||||
}) as i32
|
}) as i32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,12 +172,19 @@ impl SqliteStorage {
|
||||||
let start = day_cutoff.adding_secs(-86_400).as_millis();
|
let start = day_cutoff.adding_secs(-86_400).as_millis();
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached(include_str!("studied_today.sql"))?
|
.prepare_cached(include_str!("studied_today.sql"))?
|
||||||
.query_map([start.0, RevlogReviewKind::Manual as i64], |row| {
|
.query_map(
|
||||||
Ok(StudiedToday {
|
[
|
||||||
cards: row.get(0)?,
|
start.0,
|
||||||
seconds: row.get(1)?,
|
RevlogReviewKind::Manual as i64,
|
||||||
})
|
RevlogReviewKind::Rescheduled as i64,
|
||||||
})?
|
],
|
||||||
|
|row| {
|
||||||
|
Ok(StudiedToday {
|
||||||
|
cards: row.get(0)?,
|
||||||
|
seconds: row.get(1)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)?
|
||||||
.next()
|
.next()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
|
|
|
@ -2,4 +2,5 @@ SELECT COUNT(),
|
||||||
coalesce(sum(time) / 1000.0, 0.0)
|
coalesce(sum(time) / 1000.0, 0.0)
|
||||||
FROM revlog
|
FROM revlog
|
||||||
WHERE id > ?
|
WHERE id > ?
|
||||||
|
AND type != ?
|
||||||
AND type != ?
|
AND type != ?
|
|
@ -36,6 +36,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return tr2.cardStatsReviewLogTypeFiltered();
|
return tr2.cardStatsReviewLogTypeFiltered();
|
||||||
case ReviewKind.MANUAL:
|
case ReviewKind.MANUAL:
|
||||||
return tr2.cardStatsReviewLogTypeManual();
|
return tr2.cardStatsReviewLogTypeManual();
|
||||||
|
case ReviewKind.RESCHEDULED:
|
||||||
|
return tr2.cardStatsReviewLogTypeRescheduled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ function filterDataByTimeRange(data: DataPoint[], maxDays: number): DataPoint[]
|
||||||
export function filterRevlogEntryByReviewKind(entry: RevlogEntry): boolean {
|
export function filterRevlogEntryByReviewKind(entry: RevlogEntry): boolean {
|
||||||
return (
|
return (
|
||||||
entry.reviewKind !== RevlogEntry_ReviewKind.MANUAL
|
entry.reviewKind !== RevlogEntry_ReviewKind.MANUAL
|
||||||
|
&& entry.reviewKind !== RevlogEntry_ReviewKind.RESCHEDULED
|
||||||
&& (entry.reviewKind !== RevlogEntry_ReviewKind.FILTERED || entry.ease !== 0)
|
&& (entry.reviewKind !== RevlogEntry_ReviewKind.FILTERED || entry.ease !== 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue