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-suspend-card = Suspend Card
|
||||
actions-set-due-date = Set Due Date
|
||||
actions-grade-now = Grade Now
|
||||
actions-answer-card = Answer Card
|
||||
actions-unbury-unsuspend = Unbury/Unsuspend
|
||||
actions-add-deck = Add Deck
|
||||
|
|
|
@ -172,6 +172,11 @@ scheduling-set-due-date-done =
|
|||
[one] Set due date of { $cards } card.
|
||||
*[other] Set due date of { $cards } cards.
|
||||
}
|
||||
scheduling-graded-cards-done =
|
||||
{ $cards ->
|
||||
[one] Graded { $cards } card.
|
||||
*[other] Graded { $cards } cards.
|
||||
}
|
||||
scheduling-forgot-cards =
|
||||
{ $cards ->
|
||||
[one] Reset { $cards } card.
|
||||
|
|
|
@ -36,6 +36,7 @@ service SchedulerService {
|
|||
rpc ScheduleCardsAsNewDefaults(ScheduleCardsAsNewDefaultsRequest)
|
||||
returns (ScheduleCardsAsNewDefaultsResponse);
|
||||
rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges);
|
||||
rpc GradeNow(GradeNowRequest) returns (collection.OpChanges);
|
||||
rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount);
|
||||
rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount);
|
||||
rpc GetSchedulingStates(cards.CardId) returns (SchedulingStates);
|
||||
|
@ -238,6 +239,11 @@ message SetDueDateRequest {
|
|||
config.OptionalStringConfigKey config_key = 3;
|
||||
}
|
||||
|
||||
message GradeNowRequest {
|
||||
repeated int64 card_ids = 1;
|
||||
CardAnswer.Rating rating = 2;
|
||||
}
|
||||
|
||||
message SortCardsRequest {
|
||||
repeated int64 card_ids = 1;
|
||||
uint32 starting_from = 2;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
|
@ -37,6 +38,7 @@ from aqt.operations.note import remove_notes
|
|||
from aqt.operations.scheduling import (
|
||||
bury_cards,
|
||||
forget_cards,
|
||||
grade_now,
|
||||
reposition_new_cards_dialog,
|
||||
set_due_date_dialog,
|
||||
suspend_cards,
|
||||
|
@ -340,6 +342,7 @@ class Browser(QMainWindow):
|
|||
qconnect(f.action_Info.triggered, self.showCardInfo)
|
||||
qconnect(f.actionReposition.triggered, self.reposition)
|
||||
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.actionToggle_Suspend.triggered, self.suspend_selected_cards)
|
||||
qconnect(f.action_toggle_bury.triggered, self.bury_selected_cards)
|
||||
|
@ -1080,6 +1083,43 @@ class Browser(QMainWindow):
|
|||
):
|
||||
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
|
||||
######################################################################
|
||||
|
||||
|
|
|
@ -267,6 +267,7 @@
|
|||
<addaction name="actionChange_Deck"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_set_due_date"/>
|
||||
<addaction name="action_grade_now"/>
|
||||
<addaction name="action_forget"/>
|
||||
<addaction name="actionReposition"/>
|
||||
<addaction name="separator"/>
|
||||
|
@ -623,6 +624,14 @@
|
|||
<string notr="true">Ctrl+Shift+D</string>
|
||||
</property>
|
||||
</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">
|
||||
<property name="text">
|
||||
<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(
|
||||
*,
|
||||
parent: QWidget,
|
||||
|
|
|
@ -32,6 +32,7 @@ pub enum Op {
|
|||
ScheduleAsNew,
|
||||
SetCardDeck,
|
||||
SetDueDate,
|
||||
GradeNow,
|
||||
SetFlag,
|
||||
SortCards,
|
||||
Suspend,
|
||||
|
@ -65,6 +66,7 @@ impl Op {
|
|||
Op::RenameDeck => tr.actions_rename_deck(),
|
||||
Op::ScheduleAsNew => tr.actions_forget_card(),
|
||||
Op::SetDueDate => tr.actions_set_due_date(),
|
||||
Op::GradeNow => tr.actions_grade_now(),
|
||||
Op::Suspend => tr.studying_suspend(),
|
||||
Op::UnburyUnsuspend => tr.actions_unbury_unsuspend(),
|
||||
Op::UpdateCard => tr.actions_update_card(),
|
||||
|
|
|
@ -52,6 +52,7 @@ pub struct CardAnswer {
|
|||
pub answered_at: TimestampMillis,
|
||||
pub milliseconds_taken: u32,
|
||||
pub custom_data: Option<String>,
|
||||
pub from_queue: bool,
|
||||
}
|
||||
|
||||
impl CardAnswer {
|
||||
|
@ -312,7 +313,7 @@ impl Collection {
|
|||
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
|
||||
.storage
|
||||
.get_card(answer.card_id)?
|
||||
|
@ -363,14 +364,24 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
self.update_queues_after_answering_card(
|
||||
&card,
|
||||
timing,
|
||||
matches!(
|
||||
answer.new_state,
|
||||
CardState::Filtered(FilteredState::Preview(PreviewState { finished: true, .. }))
|
||||
),
|
||||
)
|
||||
// Handle queue updates based on from_queue flag
|
||||
if answer.from_queue {
|
||||
self.update_queues_after_answering_card(
|
||||
&card,
|
||||
timing,
|
||||
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<()> {
|
||||
|
@ -588,6 +599,7 @@ pub mod test_helpers {
|
|||
answered_at: TimestampMillis::now(),
|
||||
milliseconds_taken: 0,
|
||||
custom_data: None,
|
||||
from_queue: true,
|
||||
})?;
|
||||
Ok(PostAnswerState {
|
||||
card_id: queued.card.id,
|
||||
|
|
|
@ -96,6 +96,7 @@ mod test {
|
|||
answered_at: TimestampMillis::now(),
|
||||
milliseconds_taken: 0,
|
||||
custom_data: None,
|
||||
from_queue: true,
|
||||
})?;
|
||||
|
||||
c = col.storage.get_card(c.id)?.unwrap();
|
||||
|
@ -111,6 +112,7 @@ mod test {
|
|||
answered_at: TimestampMillis::now(),
|
||||
milliseconds_taken: 0,
|
||||
custom_data: None,
|
||||
from_queue: true,
|
||||
})?;
|
||||
c = col.storage.get_card(c.id)?.unwrap();
|
||||
assert_eq!(c.queue, CardQueue::PreviewRepeat);
|
||||
|
@ -126,6 +128,7 @@ mod test {
|
|||
answered_at: TimestampMillis::now(),
|
||||
milliseconds_taken: 0,
|
||||
custom_data: None,
|
||||
from_queue: true,
|
||||
})?;
|
||||
c = col.storage.get_card(c.id)?.unwrap();
|
||||
assert_eq!(c.queue, CardQueue::DayLearn);
|
||||
|
|
|
@ -8,6 +8,7 @@ use rand::distributions::Distribution;
|
|||
use rand::distributions::Uniform;
|
||||
use regex::Regex;
|
||||
|
||||
use super::answering::CardAnswer;
|
||||
use crate::card::Card;
|
||||
use crate::card::CardId;
|
||||
use crate::card::CardQueue;
|
||||
|
@ -143,6 +144,34 @@ impl Collection {
|
|||
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)]
|
||||
|
|
|
@ -21,6 +21,7 @@ impl From<anki_proto::scheduler::CardAnswer> for CardAnswer {
|
|||
answered_at: TimestampMillis(answer.answered_at_millis),
|
||||
milliseconds_taken: answer.milliseconds_taken,
|
||||
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)
|
||||
}
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
input: scheduler::SortCardsRequest,
|
||||
|
|
Loading…
Reference in a new issue