mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Feat/grade now (#3840)
* Feat/grade now * pass ci * fix from_queue * Refactor card answering to support from_queue flag - Add `from_queue` field to `CardAnswer` struct and proto message - Modify `answer_card_inner` to handle queue updates based on `from_queue` - Remove `grade_card` method and consolidate card answering logic - Update related test cases to set `from_queue` flag * fix current_changes() called when no op set * Optimize queue updates for batch card processing - Refactor `grade_now` to collect processed card IDs first - Add new `update_queues_for_processed_cards` method for efficient batch queue updates - Improve queue management by removing entries and updating counts in a single pass - Remove individual queue update method in favor of batch processing * pass ci * keep the same style * remove ineffective code * remove unused imports
This commit is contained in:
parent
79b6f658c3
commit
0e31efac08
12 changed files with 154 additions and 9 deletions
|
@ -50,6 +50,7 @@ actions-select = Select
|
||||||
actions-shortcut-key = Shortcut key: { $val }
|
actions-shortcut-key = Shortcut key: { $val }
|
||||||
actions-suspend-card = Suspend Card
|
actions-suspend-card = Suspend Card
|
||||||
actions-set-due-date = Set Due Date
|
actions-set-due-date = Set Due Date
|
||||||
|
actions-grade-now = Grade Now
|
||||||
actions-answer-card = Answer Card
|
actions-answer-card = Answer Card
|
||||||
actions-unbury-unsuspend = Unbury/Unsuspend
|
actions-unbury-unsuspend = Unbury/Unsuspend
|
||||||
actions-add-deck = Add Deck
|
actions-add-deck = Add Deck
|
||||||
|
|
|
@ -172,6 +172,11 @@ scheduling-set-due-date-done =
|
||||||
[one] Set due date of { $cards } card.
|
[one] Set due date of { $cards } card.
|
||||||
*[other] Set due date of { $cards } cards.
|
*[other] Set due date of { $cards } cards.
|
||||||
}
|
}
|
||||||
|
scheduling-graded-cards-done =
|
||||||
|
{ $cards ->
|
||||||
|
[one] Graded { $cards } card.
|
||||||
|
*[other] Graded { $cards } cards.
|
||||||
|
}
|
||||||
scheduling-forgot-cards =
|
scheduling-forgot-cards =
|
||||||
{ $cards ->
|
{ $cards ->
|
||||||
[one] Reset { $cards } card.
|
[one] Reset { $cards } card.
|
||||||
|
|
|
@ -36,6 +36,7 @@ service SchedulerService {
|
||||||
rpc ScheduleCardsAsNewDefaults(ScheduleCardsAsNewDefaultsRequest)
|
rpc ScheduleCardsAsNewDefaults(ScheduleCardsAsNewDefaultsRequest)
|
||||||
returns (ScheduleCardsAsNewDefaultsResponse);
|
returns (ScheduleCardsAsNewDefaultsResponse);
|
||||||
rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges);
|
rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges);
|
||||||
|
rpc GradeNow(GradeNowRequest) returns (collection.OpChanges);
|
||||||
rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount);
|
rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount);
|
||||||
rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount);
|
rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount);
|
||||||
rpc GetSchedulingStates(cards.CardId) returns (SchedulingStates);
|
rpc GetSchedulingStates(cards.CardId) returns (SchedulingStates);
|
||||||
|
@ -238,6 +239,11 @@ message SetDueDateRequest {
|
||||||
config.OptionalStringConfigKey config_key = 3;
|
config.OptionalStringConfigKey config_key = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GradeNowRequest {
|
||||||
|
repeated int64 card_ids = 1;
|
||||||
|
CardAnswer.Rating rating = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message SortCardsRequest {
|
message SortCardsRequest {
|
||||||
repeated int64 card_ids = 1;
|
repeated int64 card_ids = 1;
|
||||||
uint32 starting_from = 2;
|
uint32 starting_from = 2;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
@ -37,6 +38,7 @@ from aqt.operations.note import remove_notes
|
||||||
from aqt.operations.scheduling import (
|
from aqt.operations.scheduling import (
|
||||||
bury_cards,
|
bury_cards,
|
||||||
forget_cards,
|
forget_cards,
|
||||||
|
grade_now,
|
||||||
reposition_new_cards_dialog,
|
reposition_new_cards_dialog,
|
||||||
set_due_date_dialog,
|
set_due_date_dialog,
|
||||||
suspend_cards,
|
suspend_cards,
|
||||||
|
@ -340,6 +342,7 @@ class Browser(QMainWindow):
|
||||||
qconnect(f.action_Info.triggered, self.showCardInfo)
|
qconnect(f.action_Info.triggered, self.showCardInfo)
|
||||||
qconnect(f.actionReposition.triggered, self.reposition)
|
qconnect(f.actionReposition.triggered, self.reposition)
|
||||||
qconnect(f.action_set_due_date.triggered, self.set_due_date)
|
qconnect(f.action_set_due_date.triggered, self.set_due_date)
|
||||||
|
qconnect(f.action_grade_now.triggered, self.grade_now)
|
||||||
qconnect(f.action_forget.triggered, self.forget_cards)
|
qconnect(f.action_forget.triggered, self.forget_cards)
|
||||||
qconnect(f.actionToggle_Suspend.triggered, self.suspend_selected_cards)
|
qconnect(f.actionToggle_Suspend.triggered, self.suspend_selected_cards)
|
||||||
qconnect(f.action_toggle_bury.triggered, self.bury_selected_cards)
|
qconnect(f.action_toggle_bury.triggered, self.bury_selected_cards)
|
||||||
|
@ -1080,6 +1083,43 @@ class Browser(QMainWindow):
|
||||||
):
|
):
|
||||||
op.run_in_background()
|
op.run_in_background()
|
||||||
|
|
||||||
|
@no_arg_trigger
|
||||||
|
@skip_if_selection_is_empty
|
||||||
|
@ensure_editor_saved
|
||||||
|
def grade_now(self) -> None:
|
||||||
|
"""Show dialog to grade selected cards."""
|
||||||
|
dialog = QDialog(self)
|
||||||
|
dialog.setWindowTitle(tr.actions_grade_now())
|
||||||
|
layout = QHBoxLayout()
|
||||||
|
dialog.setLayout(layout)
|
||||||
|
|
||||||
|
# Add grade buttons
|
||||||
|
for ease, label in [
|
||||||
|
(1, tr.studying_again()),
|
||||||
|
(2, tr.studying_hard()),
|
||||||
|
(3, tr.studying_good()),
|
||||||
|
(4, tr.studying_easy()),
|
||||||
|
]:
|
||||||
|
btn = QPushButton(label)
|
||||||
|
qconnect(
|
||||||
|
btn.clicked,
|
||||||
|
functools.partial(
|
||||||
|
grade_now,
|
||||||
|
parent=self,
|
||||||
|
card_ids=self.selected_cards(),
|
||||||
|
ease=ease,
|
||||||
|
dialog=dialog,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
layout.addWidget(btn)
|
||||||
|
|
||||||
|
# Add cancel button
|
||||||
|
cancel_btn = QPushButton(tr.actions_cancel())
|
||||||
|
qconnect(cancel_btn.clicked, dialog.reject)
|
||||||
|
layout.addWidget(cancel_btn)
|
||||||
|
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
# Edit: selection
|
# Edit: selection
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
|
@ -267,6 +267,7 @@
|
||||||
<addaction name="actionChange_Deck"/>
|
<addaction name="actionChange_Deck"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_set_due_date"/>
|
<addaction name="action_set_due_date"/>
|
||||||
|
<addaction name="action_grade_now"/>
|
||||||
<addaction name="action_forget"/>
|
<addaction name="action_forget"/>
|
||||||
<addaction name="actionReposition"/>
|
<addaction name="actionReposition"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
@ -623,6 +624,14 @@
|
||||||
<string notr="true">Ctrl+Shift+D</string>
|
<string notr="true">Ctrl+Shift+D</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_grade_now">
|
||||||
|
<property name="text">
|
||||||
|
<string>actions_grade_now</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string notr="true">Ctrl+Shift+G</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
<action name="action_forget">
|
<action name="action_forget">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>qt_accel_forget</string>
|
<string>qt_accel_forget</string>
|
||||||
|
|
|
@ -65,6 +65,35 @@ def set_due_date_dialog(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def grade_now(
|
||||||
|
*,
|
||||||
|
parent: QWidget,
|
||||||
|
card_ids: Sequence[CardId],
|
||||||
|
ease: int,
|
||||||
|
dialog: QDialog,
|
||||||
|
) -> None:
|
||||||
|
if ease == 1:
|
||||||
|
rating = CardAnswer.AGAIN
|
||||||
|
elif ease == 2:
|
||||||
|
rating = CardAnswer.HARD
|
||||||
|
elif ease == 3:
|
||||||
|
rating = CardAnswer.GOOD
|
||||||
|
else:
|
||||||
|
rating = CardAnswer.EASY
|
||||||
|
CollectionOp(
|
||||||
|
parent,
|
||||||
|
lambda col: col._backend.grade_now(
|
||||||
|
card_ids=card_ids,
|
||||||
|
rating=rating,
|
||||||
|
),
|
||||||
|
).success(
|
||||||
|
lambda _: tooltip(
|
||||||
|
tr.scheduling_graded_cards_done(cards=len(card_ids)), parent=parent
|
||||||
|
)
|
||||||
|
).run_in_background()
|
||||||
|
dialog.accept()
|
||||||
|
|
||||||
|
|
||||||
def forget_cards(
|
def forget_cards(
|
||||||
*,
|
*,
|
||||||
parent: QWidget,
|
parent: QWidget,
|
||||||
|
|
|
@ -32,6 +32,7 @@ pub enum Op {
|
||||||
ScheduleAsNew,
|
ScheduleAsNew,
|
||||||
SetCardDeck,
|
SetCardDeck,
|
||||||
SetDueDate,
|
SetDueDate,
|
||||||
|
GradeNow,
|
||||||
SetFlag,
|
SetFlag,
|
||||||
SortCards,
|
SortCards,
|
||||||
Suspend,
|
Suspend,
|
||||||
|
@ -65,6 +66,7 @@ impl Op {
|
||||||
Op::RenameDeck => tr.actions_rename_deck(),
|
Op::RenameDeck => tr.actions_rename_deck(),
|
||||||
Op::ScheduleAsNew => tr.actions_forget_card(),
|
Op::ScheduleAsNew => tr.actions_forget_card(),
|
||||||
Op::SetDueDate => tr.actions_set_due_date(),
|
Op::SetDueDate => tr.actions_set_due_date(),
|
||||||
|
Op::GradeNow => tr.actions_grade_now(),
|
||||||
Op::Suspend => tr.studying_suspend(),
|
Op::Suspend => tr.studying_suspend(),
|
||||||
Op::UnburyUnsuspend => tr.actions_unbury_unsuspend(),
|
Op::UnburyUnsuspend => tr.actions_unbury_unsuspend(),
|
||||||
Op::UpdateCard => tr.actions_update_card(),
|
Op::UpdateCard => tr.actions_update_card(),
|
||||||
|
|
|
@ -52,6 +52,7 @@ pub struct CardAnswer {
|
||||||
pub answered_at: TimestampMillis,
|
pub answered_at: TimestampMillis,
|
||||||
pub milliseconds_taken: u32,
|
pub milliseconds_taken: u32,
|
||||||
pub custom_data: Option<String>,
|
pub custom_data: Option<String>,
|
||||||
|
pub from_queue: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CardAnswer {
|
impl CardAnswer {
|
||||||
|
@ -312,7 +313,7 @@ impl Collection {
|
||||||
self.transact(Op::AnswerCard, |col| col.answer_card_inner(answer))
|
self.transact(Op::AnswerCard, |col| col.answer_card_inner(answer))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn answer_card_inner(&mut self, answer: &mut CardAnswer) -> Result<()> {
|
pub(crate) fn answer_card_inner(&mut self, answer: &mut CardAnswer) -> Result<()> {
|
||||||
let card = self
|
let card = self
|
||||||
.storage
|
.storage
|
||||||
.get_card(answer.card_id)?
|
.get_card(answer.card_id)?
|
||||||
|
@ -363,14 +364,24 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_queues_after_answering_card(
|
// Handle queue updates based on from_queue flag
|
||||||
&card,
|
if answer.from_queue {
|
||||||
timing,
|
self.update_queues_after_answering_card(
|
||||||
matches!(
|
&card,
|
||||||
answer.new_state,
|
timing,
|
||||||
CardState::Filtered(FilteredState::Preview(PreviewState { finished: true, .. }))
|
matches!(
|
||||||
),
|
answer.new_state,
|
||||||
)
|
CardState::Filtered(FilteredState::Preview(PreviewState {
|
||||||
|
finished: true,
|
||||||
|
..
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
} else if card.queue == CardQueue::Suspended {
|
||||||
|
invalid_input!("Can't answer suspended cards");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maybe_bury_siblings(&mut self, card: &Card, config: &DeckConfig) -> Result<()> {
|
fn maybe_bury_siblings(&mut self, card: &Card, config: &DeckConfig) -> Result<()> {
|
||||||
|
@ -588,6 +599,7 @@ pub mod test_helpers {
|
||||||
answered_at: TimestampMillis::now(),
|
answered_at: TimestampMillis::now(),
|
||||||
milliseconds_taken: 0,
|
milliseconds_taken: 0,
|
||||||
custom_data: None,
|
custom_data: None,
|
||||||
|
from_queue: true,
|
||||||
})?;
|
})?;
|
||||||
Ok(PostAnswerState {
|
Ok(PostAnswerState {
|
||||||
card_id: queued.card.id,
|
card_id: queued.card.id,
|
||||||
|
|
|
@ -96,6 +96,7 @@ mod test {
|
||||||
answered_at: TimestampMillis::now(),
|
answered_at: TimestampMillis::now(),
|
||||||
milliseconds_taken: 0,
|
milliseconds_taken: 0,
|
||||||
custom_data: None,
|
custom_data: None,
|
||||||
|
from_queue: true,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
c = col.storage.get_card(c.id)?.unwrap();
|
c = col.storage.get_card(c.id)?.unwrap();
|
||||||
|
@ -111,6 +112,7 @@ mod test {
|
||||||
answered_at: TimestampMillis::now(),
|
answered_at: TimestampMillis::now(),
|
||||||
milliseconds_taken: 0,
|
milliseconds_taken: 0,
|
||||||
custom_data: None,
|
custom_data: None,
|
||||||
|
from_queue: true,
|
||||||
})?;
|
})?;
|
||||||
c = col.storage.get_card(c.id)?.unwrap();
|
c = col.storage.get_card(c.id)?.unwrap();
|
||||||
assert_eq!(c.queue, CardQueue::PreviewRepeat);
|
assert_eq!(c.queue, CardQueue::PreviewRepeat);
|
||||||
|
@ -126,6 +128,7 @@ mod test {
|
||||||
answered_at: TimestampMillis::now(),
|
answered_at: TimestampMillis::now(),
|
||||||
milliseconds_taken: 0,
|
milliseconds_taken: 0,
|
||||||
custom_data: None,
|
custom_data: None,
|
||||||
|
from_queue: true,
|
||||||
})?;
|
})?;
|
||||||
c = col.storage.get_card(c.id)?.unwrap();
|
c = col.storage.get_card(c.id)?.unwrap();
|
||||||
assert_eq!(c.queue, CardQueue::DayLearn);
|
assert_eq!(c.queue, CardQueue::DayLearn);
|
||||||
|
|
|
@ -8,6 +8,7 @@ use rand::distributions::Distribution;
|
||||||
use rand::distributions::Uniform;
|
use rand::distributions::Uniform;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
use super::answering::CardAnswer;
|
||||||
use crate::card::Card;
|
use crate::card::Card;
|
||||||
use crate::card::CardId;
|
use crate::card::CardId;
|
||||||
use crate::card::CardQueue;
|
use crate::card::CardQueue;
|
||||||
|
@ -143,6 +144,34 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn grade_now(&mut self, cids: &[CardId], rating: i32) -> Result<OpOutput<()>> {
|
||||||
|
self.transact(Op::GradeNow, |col| {
|
||||||
|
for &card_id in cids {
|
||||||
|
let states = col.get_scheduling_states(card_id)?;
|
||||||
|
let new_state = match rating {
|
||||||
|
0 => states.again,
|
||||||
|
1 => states.hard,
|
||||||
|
2 => states.good,
|
||||||
|
3 => states.easy,
|
||||||
|
_ => invalid_input!("invalid rating"),
|
||||||
|
};
|
||||||
|
let mut answer: CardAnswer = anki_proto::scheduler::CardAnswer {
|
||||||
|
card_id: card_id.into(),
|
||||||
|
current_state: Some(states.current.into()),
|
||||||
|
new_state: Some(new_state.into()),
|
||||||
|
rating,
|
||||||
|
milliseconds_taken: 0,
|
||||||
|
answered_at_millis: TimestampMillis::now().into(),
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
// Process the card without updating queues yet
|
||||||
|
answer.from_queue = false;
|
||||||
|
col.answer_card_inner(&mut answer)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -21,6 +21,7 @@ impl From<anki_proto::scheduler::CardAnswer> for CardAnswer {
|
||||||
answered_at: TimestampMillis(answer.answered_at_millis),
|
answered_at: TimestampMillis(answer.answered_at_millis),
|
||||||
milliseconds_taken: answer.milliseconds_taken,
|
milliseconds_taken: answer.milliseconds_taken,
|
||||||
custom_data,
|
custom_data,
|
||||||
|
from_queue: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,6 +165,14 @@ impl crate::services::SchedulerService for Collection {
|
||||||
self.set_due_date(&cids, &days, config).map(Into::into)
|
self.set_due_date(&cids, &days, config).map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grade_now(
|
||||||
|
&mut self,
|
||||||
|
input: scheduler::GradeNowRequest,
|
||||||
|
) -> Result<anki_proto::collection::OpChanges> {
|
||||||
|
self.grade_now(&input.card_ids.into_newtype(CardId), input.rating)
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
fn sort_cards(
|
fn sort_cards(
|
||||||
&mut self,
|
&mut self,
|
||||||
input: scheduler::SortCardsRequest,
|
input: scheduler::SortCardsRequest,
|
||||||
|
|
Loading…
Reference in a new issue