Rework reschedule tool
The old rescheduling dialog's two options have been split into two separate menu items, "Forget", and "Set Due Date" For cards that are not review cards, "Set Due Date" behaves like the old reschedule option, changing the cards into a review card, and and setting both the interval and due date to the provided number of days. When "Set Due Date" is applied to a review card, it no longer resets the card's interval. Instead, it looks at how much the provided number of days will change the original interval, and adjusts the interval by that amount, so that cards that are answered earlier receive a smaller next interval, and cards that are answered after a longer delay receive a bonus. For example, imagine a card was answered on day 5, and given an interval of 10 days, so it has a due date of day 15. - if on day 10 the due date is changed to day 12 (today+2), the card is being scheduled 3 days earlier than it was supposed to be, so the interval will be adjusted to 7 days. - and if on day 10 the due date is changed to day 20, the interval will be changed from 10 days to 15 days. There is no separate option to reset the interval of a review card, but it can be accomplished by forgetting the card(s), and then setting the desired due date. Other notes: - Added the action to the review screen as well. - Set the shortcut to Ctrl+Shift+D, and changed the existing Delete Tags shortcut to Ctrl+Alt+Shift+A.
This commit is contained in:
parent
c91a3784f6
commit
704b5e581a
@ -33,3 +33,4 @@ actions-search = Search
|
|||||||
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-forget = Forget
|
||||||
|
@ -66,8 +66,6 @@ browsing-optional-filter = Optional filter:
|
|||||||
browsing-override-back-template = Override back template:
|
browsing-override-back-template = Override back template:
|
||||||
browsing-override-font = Override font:
|
browsing-override-font = Override font:
|
||||||
browsing-override-front-template = Override front template:
|
browsing-override-front-template = Override front template:
|
||||||
browsing-place-at-end-of-new-card = Place at end of new card queue
|
|
||||||
browsing-place-in-review-queue-with-interval = Place in review queue with interval between:
|
|
||||||
browsing-please-give-your-filter-a-name = Please give your filter a name:
|
browsing-please-give-your-filter-a-name = Please give your filter a name:
|
||||||
browsing-please-select-cards-from-only-one = Please select cards from only one note type.
|
browsing-please-select-cards-from-only-one = Please select cards from only one note type.
|
||||||
browsing-preview-selected-card = Preview Selected Card ({ $val })
|
browsing-preview-selected-card = Preview Selected Card ({ $val })
|
||||||
|
@ -143,3 +143,19 @@ scheduling-deck-updated =
|
|||||||
[one] { $count } deck updated.
|
[one] { $count } deck updated.
|
||||||
*[other] { $count } decks updated.
|
*[other] { $count } decks updated.
|
||||||
}
|
}
|
||||||
|
scheduling-set-due-date-prompt =
|
||||||
|
{ $cards ->
|
||||||
|
[one] Show card in how many days?
|
||||||
|
*[other] Show cards in how many days? (eg 1, or 1..7)
|
||||||
|
}
|
||||||
|
scheduling-set-due-date-changed-cards =
|
||||||
|
{ $cards ->
|
||||||
|
[one] Changed card's due date.
|
||||||
|
*[other] Changed due date of { $cards } cards.
|
||||||
|
}
|
||||||
|
scheduling-set-due-date-invalid-input = Expected a number or range (eg 1, or 1..7)
|
||||||
|
scheduling-forgot-cards =
|
||||||
|
{ $cards ->
|
||||||
|
[one] { $cards } card placed at the end of the new card queue.
|
||||||
|
*[other] { $cards } cards placed at the end of the new card queue.
|
||||||
|
}
|
||||||
|
@ -24,10 +24,11 @@ qt-accel-notes = &Notes
|
|||||||
qt-accel-open-addons-folder = &Open Add-ons Folder...
|
qt-accel-open-addons-folder = &Open Add-ons Folder...
|
||||||
qt-accel-preferences = &Preferences...
|
qt-accel-preferences = &Preferences...
|
||||||
qt-accel-previous-card = &Previous Card
|
qt-accel-previous-card = &Previous Card
|
||||||
qt-accel-reschedule = &Reschedule...
|
|
||||||
qt-accel-select-all = Select &All
|
qt-accel-select-all = Select &All
|
||||||
qt-accel-select-notes = Select &Notes
|
qt-accel-select-notes = Select &Notes
|
||||||
qt-accel-support-anki = &Support Anki...
|
qt-accel-support-anki = &Support Anki...
|
||||||
qt-accel-switch-profile = &Switch Profile
|
qt-accel-switch-profile = &Switch Profile
|
||||||
qt-accel-tools = &Tools
|
qt-accel-tools = &Tools
|
||||||
qt-accel-undo = &Undo
|
qt-accel-undo = &Undo
|
||||||
|
qt-accel-set-due-date = Set &Due Date...
|
||||||
|
qt-accel-forget = &Forget
|
||||||
|
@ -1397,20 +1397,17 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
|||||||
if toBury:
|
if toBury:
|
||||||
self.bury_cards(toBury, manual=False)
|
self.bury_cards(toBury, manual=False)
|
||||||
|
|
||||||
# Resetting
|
# Resetting/rescheduling
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def schedule_cards_as_new(self, card_ids: List[int]) -> None:
|
def schedule_cards_as_new(self, card_ids: List[int]) -> None:
|
||||||
"Put cards at the end of the new queue."
|
"Put cards at the end of the new queue."
|
||||||
self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
|
self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
|
||||||
|
|
||||||
def schedule_cards_as_reviews(
|
def set_due_date(self, card_ids: List[int], days: str) -> None:
|
||||||
self, card_ids: List[int], min_interval: int, max_interval: int
|
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
||||||
) -> None:
|
`days` can be of the form '5' or '5..7'"""
|
||||||
"Make cards review cards, with a new interval randomly selected from range."
|
self.col._backend.set_due_date(card_ids=card_ids, days=days)
|
||||||
self.col._backend.schedule_cards_as_reviews(
|
|
||||||
card_ids=card_ids, min_interval=min_interval, max_interval=max_interval
|
|
||||||
)
|
|
||||||
|
|
||||||
def resetCards(self, ids: List[int]) -> None:
|
def resetCards(self, ids: List[int]) -> None:
|
||||||
"Completely reset cards for export."
|
"Completely reset cards for export."
|
||||||
@ -1430,8 +1427,12 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
|||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
|
|
||||||
|
def reschedCards(
|
||||||
|
self, card_ids: List[int], min_interval: int, max_interval: int
|
||||||
|
) -> None:
|
||||||
|
self.set_due_date(card_ids, f"{min_interval}..{max_interval}")
|
||||||
|
|
||||||
forgetCards = schedule_cards_as_new
|
forgetCards = schedule_cards_as_new
|
||||||
reschedCards = schedule_cards_as_reviews
|
|
||||||
|
|
||||||
# Repositioning new cards
|
# Repositioning new cards
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -1077,23 +1077,6 @@ def test_forget():
|
|||||||
assert col.sched.counts() == (1, 0, 0)
|
assert col.sched.counts() == (1, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_resched():
|
|
||||||
col = getEmptyCol()
|
|
||||||
note = col.newNote()
|
|
||||||
note["Front"] = "one"
|
|
||||||
col.addNote(note)
|
|
||||||
c = note.cards()[0]
|
|
||||||
col.sched.reschedCards([c.id], 0, 0)
|
|
||||||
c.load()
|
|
||||||
assert c.due == col.sched.today
|
|
||||||
assert c.ivl == 1
|
|
||||||
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
|
|
||||||
col.sched.reschedCards([c.id], 1, 1)
|
|
||||||
c.load()
|
|
||||||
assert c.due == col.sched.today + 1
|
|
||||||
assert c.ivl == +1
|
|
||||||
|
|
||||||
|
|
||||||
def test_norelearn():
|
def test_norelearn():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
|
@ -1124,10 +1124,17 @@ 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
|
||||||
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
|
||||||
assert c.ivl == +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)
|
||||||
|
c.load()
|
||||||
|
assert c.due == col.sched.today + 1
|
||||||
|
assert c.ivl == 1
|
||||||
|
|
||||||
|
|
||||||
def test_norelearn():
|
def test_norelearn():
|
||||||
|
@ -28,6 +28,7 @@ from aqt.main import ResetReason
|
|||||||
from aqt.previewer import BrowserPreviewer as PreviewDialog
|
from aqt.previewer import BrowserPreviewer as PreviewDialog
|
||||||
from aqt.previewer import Previewer
|
from aqt.previewer import Previewer
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
from aqt.scheduling import forget_cards, set_due_date_dialog
|
||||||
from aqt.sidebar import SidebarSearchBar, SidebarTreeView
|
from aqt.sidebar import SidebarSearchBar, SidebarTreeView
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
@ -503,7 +504,8 @@ class Browser(QMainWindow):
|
|||||||
qconnect(f.actionChange_Deck.triggered, self.setDeck)
|
qconnect(f.actionChange_Deck.triggered, self.setDeck)
|
||||||
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.actionReschedule.triggered, self.reschedule)
|
qconnect(f.action_set_due_date.triggered, self.set_due_date)
|
||||||
|
qconnect(f.action_forget.triggered, self.forget_cards)
|
||||||
qconnect(f.actionToggle_Suspend.triggered, self.onSuspend)
|
qconnect(f.actionToggle_Suspend.triggered, self.onSuspend)
|
||||||
qconnect(f.actionRed_Flag.triggered, lambda: self.onSetFlag(1))
|
qconnect(f.actionRed_Flag.triggered, lambda: self.onSetFlag(1))
|
||||||
qconnect(f.actionOrange_Flag.triggered, lambda: self.onSetFlag(2))
|
qconnect(f.actionOrange_Flag.triggered, lambda: self.onSetFlag(2))
|
||||||
@ -1384,32 +1386,33 @@ where id in %s"""
|
|||||||
self.mw.requireReset(reason=ResetReason.BrowserReposition, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserReposition, context=self)
|
||||||
self.model.endReset()
|
self.model.endReset()
|
||||||
|
|
||||||
# Rescheduling
|
# Scheduling
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def reschedule(self) -> None:
|
def _after_schedule(self) -> None:
|
||||||
self.editor.saveNow(self._reschedule)
|
self.model.reset()
|
||||||
|
|
||||||
def _reschedule(self) -> None:
|
|
||||||
d = QDialog(self)
|
|
||||||
disable_help_button(d)
|
|
||||||
d.setWindowModality(Qt.WindowModal)
|
|
||||||
frm = aqt.forms.reschedule.Ui_Dialog()
|
|
||||||
frm.setupUi(d)
|
|
||||||
if not d.exec_():
|
|
||||||
return
|
|
||||||
self.model.beginReset()
|
|
||||||
self.mw.checkpoint(tr(TR.BROWSING_RESCHEDULE))
|
|
||||||
if frm.asNew.isChecked():
|
|
||||||
self.col.sched.forgetCards(self.selectedCards())
|
|
||||||
else:
|
|
||||||
fmin = frm.min.value()
|
|
||||||
fmax = frm.max.value()
|
|
||||||
fmax = max(fmin, fmax)
|
|
||||||
self.col.sched.reschedCards(self.selectedCards(), fmin, fmax)
|
|
||||||
self.search()
|
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserReschedule, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserReschedule, context=self)
|
||||||
self.model.endReset()
|
|
||||||
|
def set_due_date(self) -> None:
|
||||||
|
self.editor.saveNow(
|
||||||
|
lambda: set_due_date_dialog(
|
||||||
|
mw=self.mw,
|
||||||
|
parent=self,
|
||||||
|
card_ids=self.selectedCards(),
|
||||||
|
default="0",
|
||||||
|
on_done=self._after_schedule,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def forget_cards(self) -> None:
|
||||||
|
self.editor.saveNow(
|
||||||
|
lambda: forget_cards(
|
||||||
|
mw=self.mw,
|
||||||
|
parent=self,
|
||||||
|
card_ids=self.selectedCards(),
|
||||||
|
on_done=self._after_schedule,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Edit: selection
|
# Edit: selection
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -32,7 +32,6 @@ from . import preview
|
|||||||
from . import profiles
|
from . import profiles
|
||||||
from . import progress
|
from . import progress
|
||||||
from . import reposition
|
from . import reposition
|
||||||
from . import reschedule
|
|
||||||
from . import setgroup
|
from . import setgroup
|
||||||
from . import setlang
|
from . import setlang
|
||||||
from . import stats
|
from . import stats
|
||||||
|
@ -262,7 +262,8 @@
|
|||||||
</widget>
|
</widget>
|
||||||
<addaction name="actionChange_Deck"/>
|
<addaction name="actionChange_Deck"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionReschedule"/>
|
<addaction name="action_set_due_date"/>
|
||||||
|
<addaction name="action_forget"/>
|
||||||
<addaction name="actionReposition"/>
|
<addaction name="actionReposition"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionToggle_Suspend"/>
|
<addaction name="actionToggle_Suspend"/>
|
||||||
@ -298,14 +299,6 @@
|
|||||||
<addaction name="menuJump"/>
|
<addaction name="menuJump"/>
|
||||||
<addaction name="menu_Help"/>
|
<addaction name="menu_Help"/>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="actionReschedule">
|
|
||||||
<property name="text">
|
|
||||||
<string>QT_ACCEL_RESCHEDULE</string>
|
|
||||||
</property>
|
|
||||||
<property name="shortcut">
|
|
||||||
<string notr="true">Ctrl+Alt+R</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionSelectAll">
|
<action name="actionSelectAll">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>QT_ACCEL_SELECT_ALL</string>
|
<string>QT_ACCEL_SELECT_ALL</string>
|
||||||
@ -465,7 +458,7 @@
|
|||||||
<string>BROWSING_REMOVE_TAGS</string>
|
<string>BROWSING_REMOVE_TAGS</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut">
|
||||||
<string notr="true">Ctrl+Shift+D</string>
|
<string notr="true">Ctrl+Alt+Shift+A</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionToggle_Suspend">
|
<action name="actionToggle_Suspend">
|
||||||
@ -586,6 +579,19 @@
|
|||||||
<string notr="true">Ctrl+G</string>
|
<string notr="true">Ctrl+G</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_set_due_date">
|
||||||
|
<property name="text">
|
||||||
|
<string>QT_ACCEL_SET_DUE_DATE</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string notr="true">Ctrl+Shift+D</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="action_forget">
|
||||||
|
<property name="text">
|
||||||
|
<string>QT_ACCEL_FORGET</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="icons.qrc"/>
|
<include location="icons.qrc"/>
|
||||||
|
@ -18,33 +18,3 @@ outdata = re.sub(
|
|||||||
|
|
||||||
with open(py_file, "w") as file:
|
with open(py_file, "w") as file:
|
||||||
file.write(outdata)
|
file.write(outdata)
|
||||||
|
|
||||||
# init=aqt/forms/__init__.py
|
|
||||||
# temp=aqt/forms/scratch
|
|
||||||
# rm -f $init $temp
|
|
||||||
# echo "# This file auto-generated by build_ui.sh. Don't edit." > $init
|
|
||||||
# echo "__all__ = [" >> $init
|
|
||||||
|
|
||||||
# echo "Generating forms.."
|
|
||||||
# for i in designer/*.ui
|
|
||||||
# do
|
|
||||||
# base=$(basename $i .ui)
|
|
||||||
# py="aqt/forms/${base}.py"
|
|
||||||
# echo " \"$base\"," >> $init
|
|
||||||
# echo "from . import $base" >> $temp
|
|
||||||
# if [ $i -nt $py ]; then
|
|
||||||
# echo " * "$py
|
|
||||||
# pyuic5 --from-imports $i -o $py.tmp
|
|
||||||
# (cat <<EOF; tail -n +3 $py.tmp) | perl -p -e 's/(QtGui\.QApplication\.)?_?translate\(".*?", /_(/; s/, None.*/))/' > $py
|
|
||||||
# # -*- coding: utf-8 -*-
|
|
||||||
# # pylint: disable=unsubscriptable-object,unused-import
|
|
||||||
# # EOF
|
|
||||||
# rm $py.tmp
|
|
||||||
# fi
|
|
||||||
# done
|
|
||||||
# echo "]" >> $init
|
|
||||||
# cat $temp >> $init
|
|
||||||
# rm $temp
|
|
||||||
|
|
||||||
# echo "Building resources.."
|
|
||||||
# pyrcc5 designer/icons.qrc -o aqt/forms/icons_rc.py
|
|
@ -1,183 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>Dialog</class>
|
|
||||||
<widget class="QDialog" name="Dialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>325</width>
|
|
||||||
<height>144</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>BROWSING_RESCHEDULE</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
|
||||||
<item>
|
|
||||||
<widget class="QRadioButton" name="asNew">
|
|
||||||
<property name="text">
|
|
||||||
<string>BROWSING_PLACE_AT_END_OF_NEW_CARD</string>
|
|
||||||
</property>
|
|
||||||
<property name="checked">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QRadioButton" name="asRev">
|
|
||||||
<property name="text">
|
|
||||||
<string>BROWSING_PLACE_IN_REVIEW_QUEUE_WITH_INTERVAL</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QWidget" name="rangebox" native="true">
|
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<property name="leftMargin">
|
|
||||||
<number>20</number>
|
|
||||||
</property>
|
|
||||||
<property name="topMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="rightMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<property name="bottomMargin">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string notr="true">~</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QSpinBox" name="min">
|
|
||||||
<property name="maximum">
|
|
||||||
<number>9999</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="2">
|
|
||||||
<widget class="QSpinBox" name="max">
|
|
||||||
<property name="maximum">
|
|
||||||
<number>9999</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="3">
|
|
||||||
<widget class="QLabel" name="label_4">
|
|
||||||
<property name="text">
|
|
||||||
<string>SCHEDULING_DAYS</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="4">
|
|
||||||
<spacer name="horizontalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<tabstops>
|
|
||||||
<tabstop>asNew</tabstop>
|
|
||||||
<tabstop>asRev</tabstop>
|
|
||||||
<tabstop>min</tabstop>
|
|
||||||
<tabstop>max</tabstop>
|
|
||||||
<tabstop>buttonBox</tabstop>
|
|
||||||
</tabstops>
|
|
||||||
<resources/>
|
|
||||||
<connections>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>accepted()</signal>
|
|
||||||
<receiver>Dialog</receiver>
|
|
||||||
<slot>accept()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>222</x>
|
|
||||||
<y>144</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>157</x>
|
|
||||||
<y>157</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>rejected()</signal>
|
|
||||||
<receiver>Dialog</receiver>
|
|
||||||
<slot>reject()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>222</x>
|
|
||||||
<y>150</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>226</x>
|
|
||||||
<y>157</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
<connection>
|
|
||||||
<sender>asRev</sender>
|
|
||||||
<signal>toggled(bool)</signal>
|
|
||||||
<receiver>rangebox</receiver>
|
|
||||||
<slot>setEnabled(bool)</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>30</x>
|
|
||||||
<y>40</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>11</x>
|
|
||||||
<y>79</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
|
||||||
</ui>
|
|
@ -18,6 +18,7 @@ from anki.utils import stripHTML
|
|||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.profiles import VideoDriver
|
from aqt.profiles import VideoDriver
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
from aqt.scheduling import set_due_date_dialog
|
||||||
from aqt.sound import av_player, play_clicked_audio, record_audio
|
from aqt.sound import av_player, play_clicked_audio, record_audio
|
||||||
from aqt.theme import theme_manager
|
from aqt.theme import theme_manager
|
||||||
from aqt.toolbar import BottomBar
|
from aqt.toolbar import BottomBar
|
||||||
@ -300,6 +301,7 @@ class Reviewer:
|
|||||||
("!", self.onSuspend),
|
("!", self.onSuspend),
|
||||||
("@", self.onSuspendCard),
|
("@", self.onSuspendCard),
|
||||||
("Ctrl+Delete", self.onDelete),
|
("Ctrl+Delete", self.onDelete),
|
||||||
|
("Ctrl+Shift+D", self.on_set_due),
|
||||||
("v", self.onReplayRecorded),
|
("v", self.onReplayRecorded),
|
||||||
("Shift+v", self.onRecordVoice),
|
("Shift+v", self.onRecordVoice),
|
||||||
("o", self.onOptions),
|
("o", self.onOptions),
|
||||||
@ -732,6 +734,7 @@ time = %(time)d;
|
|||||||
[tr(TR.STUDYING_MARK_NOTE), "*", self.onMark],
|
[tr(TR.STUDYING_MARK_NOTE), "*", self.onMark],
|
||||||
[tr(TR.STUDYING_BURY_CARD), "-", self.onBuryCard],
|
[tr(TR.STUDYING_BURY_CARD), "-", self.onBuryCard],
|
||||||
[tr(TR.STUDYING_BURY_NOTE), "=", self.onBuryNote],
|
[tr(TR.STUDYING_BURY_NOTE), "=", self.onBuryNote],
|
||||||
|
[tr(TR.ACTIONS_SET_DUE_DATE), "Ctrl+Shift+D", self.on_set_due],
|
||||||
[tr(TR.ACTIONS_SUSPEND_CARD), "@", self.onSuspendCard],
|
[tr(TR.ACTIONS_SUSPEND_CARD), "@", self.onSuspendCard],
|
||||||
[tr(TR.STUDYING_SUSPEND_NOTE), "!", self.onSuspend],
|
[tr(TR.STUDYING_SUSPEND_NOTE), "!", self.onSuspend],
|
||||||
[tr(TR.STUDYING_DELETE_NOTE), "Ctrl+Delete", self.onDelete],
|
[tr(TR.STUDYING_DELETE_NOTE), "Ctrl+Delete", self.onDelete],
|
||||||
@ -798,6 +801,18 @@ time = %(time)d;
|
|||||||
f.flush()
|
f.flush()
|
||||||
self._drawMark()
|
self._drawMark()
|
||||||
|
|
||||||
|
def on_set_due(self) -> None:
|
||||||
|
if self.mw.state != "review" or not self.card:
|
||||||
|
return
|
||||||
|
|
||||||
|
set_due_date_dialog(
|
||||||
|
mw=self.mw,
|
||||||
|
parent=self.mw,
|
||||||
|
card_ids=[self.card.id],
|
||||||
|
default="1",
|
||||||
|
on_done=self.mw.reset,
|
||||||
|
)
|
||||||
|
|
||||||
def onSuspend(self) -> None:
|
def onSuspend(self) -> None:
|
||||||
self.mw.checkpoint(tr(TR.STUDYING_SUSPEND))
|
self.mw.checkpoint(tr(TR.STUDYING_SUSPEND))
|
||||||
self.mw.col.sched.suspend_cards([c.id for c in self.card.note().cards()])
|
self.mw.col.sched.suspend_cards([c.id for c in self.card.note().cards()])
|
||||||
|
80
qt/aqt/scheduling.py
Normal file
80
qt/aqt/scheduling.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from concurrent.futures import Future
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import aqt
|
||||||
|
from anki.errors import InvalidInput
|
||||||
|
from anki.lang import TR
|
||||||
|
from aqt.qt import *
|
||||||
|
from aqt.utils import getText, showWarning, tooltip, tr
|
||||||
|
|
||||||
|
|
||||||
|
def set_due_date_dialog(
|
||||||
|
*,
|
||||||
|
mw: aqt.AnkiQt,
|
||||||
|
parent: QDialog,
|
||||||
|
card_ids: List[int],
|
||||||
|
default: str,
|
||||||
|
on_done: Callable[[], None],
|
||||||
|
) -> None:
|
||||||
|
if not card_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
(days, success) = getText(
|
||||||
|
prompt=tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)),
|
||||||
|
parent=parent,
|
||||||
|
default=default,
|
||||||
|
title=tr(TR.ACTIONS_SET_DUE_DATE),
|
||||||
|
)
|
||||||
|
if not success or not days.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_done_wrapper(fut: Future) -> None:
|
||||||
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, InvalidInput):
|
||||||
|
err = tr(TR.SCHEDULING_SET_DUE_DATE_INVALID_INPUT)
|
||||||
|
else:
|
||||||
|
err = str(e)
|
||||||
|
showWarning(err)
|
||||||
|
on_done()
|
||||||
|
return
|
||||||
|
|
||||||
|
tooltip(
|
||||||
|
tr(TR.SCHEDULING_SET_DUE_DATE_CHANGED_CARDS, cards=len(card_ids)),
|
||||||
|
parent=parent,
|
||||||
|
)
|
||||||
|
|
||||||
|
on_done()
|
||||||
|
|
||||||
|
mw.checkpoint(tr(TR.ACTIONS_SET_DUE_DATE))
|
||||||
|
mw.taskman.with_progress(
|
||||||
|
lambda: mw.col.sched.set_due_date(card_ids, days), on_done_wrapper
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def forget_cards(
|
||||||
|
*, mw: aqt.AnkiQt, parent: QDialog, card_ids: List[int], on_done: Callable[[], None]
|
||||||
|
) -> None:
|
||||||
|
if not card_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_done_wrapper(fut: Future) -> None:
|
||||||
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception as e:
|
||||||
|
showWarning(str(e))
|
||||||
|
else:
|
||||||
|
tooltip(tr(TR.SCHEDULING_FORGOT_CARDS, cards=len(card_ids)), parent=parent)
|
||||||
|
|
||||||
|
on_done()
|
||||||
|
|
||||||
|
mw.checkpoint(tr(TR.ACTIONS_FORGET))
|
||||||
|
mw.taskman.with_progress(
|
||||||
|
lambda: mw.col.sched.schedule_cards_as_new(card_ids), on_done_wrapper
|
||||||
|
)
|
@ -397,6 +397,7 @@ def getText(
|
|||||||
geomKey: Optional[str] = None,
|
geomKey: Optional[str] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Tuple[str, int]:
|
) -> Tuple[str, int]:
|
||||||
|
"Returns (string, succeeded)."
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||||
d = GetTextDialog(
|
d = GetTextDialog(
|
||||||
|
@ -113,8 +113,8 @@ service BackendService {
|
|||||||
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (Empty);
|
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (Empty);
|
||||||
rpc EmptyFilteredDeck(DeckID) returns (Empty);
|
rpc EmptyFilteredDeck(DeckID) returns (Empty);
|
||||||
rpc RebuildFilteredDeck(DeckID) returns (UInt32);
|
rpc RebuildFilteredDeck(DeckID) returns (UInt32);
|
||||||
rpc ScheduleCardsAsReviews(ScheduleCardsAsReviewsIn) returns (Empty);
|
|
||||||
rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (Empty);
|
rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (Empty);
|
||||||
|
rpc SetDueDate(SetDueDateIn) returns (Empty);
|
||||||
rpc SortCards(SortCardsIn) returns (Empty);
|
rpc SortCards(SortCardsIn) returns (Empty);
|
||||||
rpc SortDeck(SortDeckIn) returns (Empty);
|
rpc SortDeck(SortDeckIn) returns (Empty);
|
||||||
|
|
||||||
@ -1188,17 +1188,16 @@ message BuryOrSuspendCardsIn {
|
|||||||
Mode mode = 2;
|
Mode mode = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ScheduleCardsAsReviewsIn {
|
|
||||||
repeated int64 card_ids = 1;
|
|
||||||
uint32 min_interval = 2;
|
|
||||||
uint32 max_interval = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ScheduleCardsAsNewIn {
|
message ScheduleCardsAsNewIn {
|
||||||
repeated int64 card_ids = 1;
|
repeated int64 card_ids = 1;
|
||||||
bool log = 2;
|
bool log = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetDueDateIn {
|
||||||
|
repeated int64 card_ids = 1;
|
||||||
|
string days = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message SortCardsIn {
|
message SortCardsIn {
|
||||||
repeated int64 card_ids = 1;
|
repeated int64 card_ids = 1;
|
||||||
uint32 starting_from = 2;
|
uint32 starting_from = 2;
|
||||||
|
@ -31,8 +31,11 @@ use crate::{
|
|||||||
all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeID, NoteTypeSchema11,
|
all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeID, NoteTypeSchema11,
|
||||||
RenderCardOutput,
|
RenderCardOutput,
|
||||||
},
|
},
|
||||||
sched::new::NewCardSortOrder,
|
sched::{
|
||||||
sched::timespan::{answer_button_time, time_span},
|
new::NewCardSortOrder,
|
||||||
|
parse_due_date_str,
|
||||||
|
timespan::{answer_button_time, time_span},
|
||||||
|
},
|
||||||
search::{
|
search::{
|
||||||
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
|
||||||
BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind,
|
BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind,
|
||||||
@ -161,6 +164,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
|
|||||||
AnkiError::DeckIsFiltered => V::DeckIsFiltered(Empty {}),
|
AnkiError::DeckIsFiltered => V::DeckIsFiltered(Empty {}),
|
||||||
AnkiError::SearchError(_) => V::InvalidInput(pb::Empty {}),
|
AnkiError::SearchError(_) => V::InvalidInput(pb::Empty {}),
|
||||||
AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}),
|
AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}),
|
||||||
|
AnkiError::ParseNumError => V::InvalidInput(pb::Empty {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
pb::BackendError {
|
pb::BackendError {
|
||||||
@ -660,18 +664,6 @@ impl BackendService for Backend {
|
|||||||
self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into))
|
self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_cards_as_reviews(
|
|
||||||
&self,
|
|
||||||
input: pb::ScheduleCardsAsReviewsIn,
|
|
||||||
) -> BackendResult<Empty> {
|
|
||||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
|
||||||
let (min, max) = (input.min_interval, input.max_interval);
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.reschedule_cards_as_reviews(&cids, min, max)
|
|
||||||
.map(Into::into)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> BackendResult<Empty> {
|
fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> BackendResult<Empty> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
||||||
@ -680,6 +672,12 @@ impl BackendService for Backend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult<pb::Empty> {
|
||||||
|
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
||||||
|
let (min, max) = parse_due_date_str(&input.days)?;
|
||||||
|
self.with_col(|col| col.set_due_date(&cids, min, max).map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult<Empty> {
|
fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult<Empty> {
|
||||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
||||||
let (start, step, random, shift) = (
|
let (start, step, random, shift) = (
|
||||||
|
@ -5,7 +5,7 @@ use crate::i18n::{tr_args, tr_strs, I18n, TR};
|
|||||||
pub use failure::{Error, Fail};
|
pub use failure::{Error, Fail};
|
||||||
use nom::error::{ErrorKind as NomErrorKind, ParseError as NomParseError};
|
use nom::error::{ErrorKind as NomErrorKind, ParseError as NomParseError};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use std::{io, str::Utf8Error};
|
use std::{io, num::ParseIntError, str::Utf8Error};
|
||||||
use tempfile::PathPersistError;
|
use tempfile::PathPersistError;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AnkiError>;
|
pub type Result<T> = std::result::Result<T, AnkiError>;
|
||||||
@ -42,6 +42,9 @@ pub enum AnkiError {
|
|||||||
#[fail(display = "Protobuf encode/decode error: {}", info)]
|
#[fail(display = "Protobuf encode/decode error: {}", info)]
|
||||||
ProtoError { info: String },
|
ProtoError { info: String },
|
||||||
|
|
||||||
|
#[fail(display = "Unable to parse number")]
|
||||||
|
ParseNumError,
|
||||||
|
|
||||||
#[fail(display = "The user interrupted the operation.")]
|
#[fail(display = "The user interrupted the operation.")]
|
||||||
Interrupted,
|
Interrupted,
|
||||||
|
|
||||||
@ -467,3 +470,9 @@ impl<'a> NomParseError<&'a str> for ParseError<'a> {
|
|||||||
other
|
other
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ParseIntError> for AnkiError {
|
||||||
|
fn from(_err: ParseIntError) -> Self {
|
||||||
|
AnkiError::ParseNumError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -64,6 +64,17 @@ impl Card {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns original_due if set, else due.
|
||||||
|
/// original_due will be set in filtered decks, and in relearning in
|
||||||
|
/// the old scheduler.
|
||||||
|
pub(crate) fn original_or_current_due(&self) -> i32 {
|
||||||
|
if self.original_due > 0 {
|
||||||
|
self.original_due
|
||||||
|
} else {
|
||||||
|
self.due
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
|
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
|
||||||
if self.original_deck_id.0 == 0 {
|
if self.original_deck_id.0 == 0 {
|
||||||
// not in a filtered deck
|
// not in a filtered deck
|
||||||
|
@ -61,35 +61,20 @@ impl RevlogEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Card {
|
|
||||||
fn last_interval_for_revlog_todo(&self) -> i32 {
|
|
||||||
self.interval as i32
|
|
||||||
|
|
||||||
// fixme: need to pass in delays for (re)learning
|
|
||||||
// if let Some(delay) = self.current_learning_delay_seconds(&[]) {
|
|
||||||
// -(delay as i32)
|
|
||||||
// } else {
|
|
||||||
// self.interval as i32
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub(crate) fn log_manually_scheduled_review(
|
pub(crate) fn log_manually_scheduled_review(
|
||||||
&mut self,
|
&mut self,
|
||||||
card: &Card,
|
card: &Card,
|
||||||
|
original: &Card,
|
||||||
usn: Usn,
|
usn: Usn,
|
||||||
next_interval: u32,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
println!("fixme: learning last_interval");
|
|
||||||
// let deck = self.get_deck(card.deck_id)?.ok_or(AnkiError::NotFound)?;
|
|
||||||
let entry = RevlogEntry {
|
let entry = RevlogEntry {
|
||||||
id: TimestampMillis::now(),
|
id: TimestampMillis::now(),
|
||||||
cid: card.id,
|
cid: card.id,
|
||||||
usn,
|
usn,
|
||||||
button_chosen: 0,
|
button_chosen: 0,
|
||||||
interval: next_interval as i32,
|
interval: card.interval as i32,
|
||||||
last_interval: card.last_interval_for_revlog_todo(),
|
last_interval: original.interval as i32,
|
||||||
ease_factor: card.ease_factor as u32,
|
ease_factor: card.ease_factor as u32,
|
||||||
taken_millis: 0,
|
taken_millis: 0,
|
||||||
review_kind: RevlogReviewKind::Manual,
|
review_kind: RevlogReviewKind::Manual,
|
||||||
|
@ -24,12 +24,7 @@ impl Card {
|
|||||||
} else {
|
} else {
|
||||||
self.queue = match self.ctype {
|
self.queue = match self.ctype {
|
||||||
CardType::Learn | CardType::Relearn => {
|
CardType::Learn | CardType::Relearn => {
|
||||||
let original_due = if self.original_due > 0 {
|
if self.original_or_current_due() > 1_000_000_000 {
|
||||||
self.original_due
|
|
||||||
} else {
|
|
||||||
self.due
|
|
||||||
};
|
|
||||||
if original_due > 1_000_000_000 {
|
|
||||||
// previous interval was in seconds
|
// previous interval was in seconds
|
||||||
CardQueue::Learn
|
CardQueue::Learn
|
||||||
} else {
|
} else {
|
||||||
|
@ -16,6 +16,7 @@ use cutoff::{
|
|||||||
sched_timing_today, v1_creation_date_adjusted_to_hour, v1_rollover_from_creation_stamp,
|
sched_timing_today, v1_creation_date_adjusted_to_hour, v1_rollover_from_creation_stamp,
|
||||||
SchedTimingToday,
|
SchedTimingToday,
|
||||||
};
|
};
|
||||||
|
pub use reviews::parse_due_date_str;
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn timing_today(&self) -> Result<SchedTimingToday> {
|
pub fn timing_today(&self) -> Result<SchedTimingToday> {
|
||||||
|
@ -110,10 +110,10 @@ impl Collection {
|
|||||||
let cards = col.storage.all_searched_cards_in_search_order()?;
|
let cards = col.storage.all_searched_cards_in_search_order()?;
|
||||||
for mut card in cards {
|
for mut card in cards {
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
if log {
|
|
||||||
col.log_manually_scheduled_review(&card, usn, 0)?;
|
|
||||||
}
|
|
||||||
card.schedule_as_new(position);
|
card.schedule_as_new(position);
|
||||||
|
if log {
|
||||||
|
col.log_manually_scheduled_review(&card, &original, usn)?;
|
||||||
|
}
|
||||||
col.update_card(&mut card, &original, usn)?;
|
col.update_card(&mut card, &original, usn)?;
|
||||||
position += 1;
|
position += 1;
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,52 @@ use crate::{
|
|||||||
collection::Collection,
|
collection::Collection,
|
||||||
deckconf::INITIAL_EASE_FACTOR_THOUSANDS,
|
deckconf::INITIAL_EASE_FACTOR_THOUSANDS,
|
||||||
err::Result,
|
err::Result,
|
||||||
|
prelude::AnkiError,
|
||||||
};
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use rand::distributions::{Distribution, Uniform};
|
use rand::distributions::{Distribution, Uniform};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
impl Card {
|
impl Card {
|
||||||
fn schedule_as_review(&mut self, interval: u32, today: u32) {
|
/// Make card due in `days_from_today`.
|
||||||
|
/// If card is not a review card, convert it into one.
|
||||||
|
/// Relearning cards have their interval preserved. Normal review
|
||||||
|
/// cards have their interval adjusted based on change between the
|
||||||
|
/// previous and new due date.
|
||||||
|
fn set_due_date(&mut self, today: u32, days_from_today: u32) {
|
||||||
|
let new_due = (today + days_from_today) as i32;
|
||||||
|
let new_interval = if let Some(old_due) = self.current_review_due_day() {
|
||||||
|
// review cards have their interval shifted based on actual elapsed time
|
||||||
|
let days_early = old_due - new_due;
|
||||||
|
((self.interval as i32) - days_early).max(0) as u32
|
||||||
|
} else if self.ctype == CardType::Relearn {
|
||||||
|
// We can't know how early or late this card entered relearning
|
||||||
|
// without consulting the revlog, which may not exist. If the user
|
||||||
|
// has their deck set up to reduce but not zero the interval on
|
||||||
|
// failure, the card may potentially have an interval of weeks or
|
||||||
|
// months, so we'll favour that if it's larger than the chosen
|
||||||
|
// `days_from_today`
|
||||||
|
self.interval.max(days_from_today)
|
||||||
|
} else {
|
||||||
|
// other cards are given a new starting interval
|
||||||
|
days_from_today
|
||||||
|
};
|
||||||
|
|
||||||
|
self.schedule_as_review(new_interval, new_due);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For review cards not in relearning, return the day the card is due.
|
||||||
|
fn current_review_due_day(&self) -> Option<i32> {
|
||||||
|
match self.ctype {
|
||||||
|
CardType::New | CardType::Learn | CardType::Relearn => None,
|
||||||
|
CardType::Review => Some(self.original_or_current_due()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_as_review(&mut self, interval: u32, due: i32) {
|
||||||
self.remove_from_filtered_deck_before_reschedule();
|
self.remove_from_filtered_deck_before_reschedule();
|
||||||
self.interval = interval.max(1);
|
self.interval = interval.max(1);
|
||||||
self.due = (today + interval) as i32;
|
self.due = due;
|
||||||
self.ctype = CardType::Review;
|
self.ctype = CardType::Review;
|
||||||
self.queue = CardQueue::Review;
|
self.queue = CardQueue::Review;
|
||||||
if self.ease_factor == 0 {
|
if self.ease_factor == 0 {
|
||||||
@ -24,13 +62,34 @@ impl Card {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a number or range (eg '4' or '4..7') into min and max.
|
||||||
|
pub fn parse_due_date_str(s: &str) -> Result<(u32, u32)> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref SINGLE: Regex = Regex::new(r#"^\d+$"#).unwrap();
|
||||||
|
static ref RANGE: Regex = Regex::new(
|
||||||
|
r#"(?x)^
|
||||||
|
(\d+)
|
||||||
|
\.\.
|
||||||
|
(\d+)
|
||||||
|
$
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
if SINGLE.is_match(s) {
|
||||||
|
let num: u32 = s.parse()?;
|
||||||
|
Ok((num, num))
|
||||||
|
} else if let Some(cap) = RANGE.captures_iter(s).next() {
|
||||||
|
let one: u32 = cap[1].parse()?;
|
||||||
|
let two: u32 = cap[2].parse()?;
|
||||||
|
Ok((one.min(two), two.max(one)))
|
||||||
|
} else {
|
||||||
|
Err(AnkiError::ParseNumError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn reschedule_cards_as_reviews(
|
pub fn set_due_date(&mut self, cids: &[CardID], min_days: u32, max_days: u32) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
cids: &[CardID],
|
|
||||||
min_days: u32,
|
|
||||||
max_days: u32,
|
|
||||||
) -> 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();
|
||||||
@ -39,9 +98,9 @@ impl Collection {
|
|||||||
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 interval = distribution.sample(&mut rng);
|
let days_from_today = distribution.sample(&mut rng);
|
||||||
col.log_manually_scheduled_review(&card, usn, interval.max(1))?;
|
card.set_due_date(today, days_from_today);
|
||||||
card.schedule_as_review(interval, today);
|
col.log_manually_scheduled_review(&card, &original, usn)?;
|
||||||
col.update_card(&mut card, &original, usn)?;
|
col.update_card(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
col.storage.clear_searched_cards_table()?;
|
col.storage.clear_searched_cards_table()?;
|
||||||
@ -49,3 +108,71 @@ impl Collection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse() -> Result<()> {
|
||||||
|
assert!(parse_due_date_str("").is_err());
|
||||||
|
assert!(parse_due_date_str("x").is_err());
|
||||||
|
assert!(parse_due_date_str("-5").is_err());
|
||||||
|
assert_eq!(parse_due_date_str("5")?, (5, 5));
|
||||||
|
assert_eq!(parse_due_date_str("50..70")?, (50, 70));
|
||||||
|
assert_eq!(parse_due_date_str("70..50")?, (50, 70));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn due_date() {
|
||||||
|
let mut c = Card::new(NoteID(0), 0, DeckID(0), 0);
|
||||||
|
|
||||||
|
// setting the due date of a new card will convert it
|
||||||
|
c.set_due_date(5, 2);
|
||||||
|
assert_eq!(c.ctype, CardType::Review);
|
||||||
|
assert_eq!(c.due, 7);
|
||||||
|
assert_eq!(c.interval, 2);
|
||||||
|
|
||||||
|
// reschedule it again the next day, shifting it from day 7 to day 9
|
||||||
|
c.set_due_date(6, 3);
|
||||||
|
assert_eq!(c.due, 9);
|
||||||
|
// we moved it 2 days forward from its original 2 day interval, and the
|
||||||
|
// interval should match the new delay
|
||||||
|
assert_eq!(c.interval, 4);
|
||||||
|
|
||||||
|
// we can bring cards forward too - return it to its original due date
|
||||||
|
c.set_due_date(6, 1);
|
||||||
|
assert_eq!(c.due, 7);
|
||||||
|
assert_eq!(c.interval, 2);
|
||||||
|
|
||||||
|
// should work in a filtered deck
|
||||||
|
c.original_due = 7;
|
||||||
|
c.original_deck_id = DeckID(1);
|
||||||
|
c.due = -10000;
|
||||||
|
c.queue = CardQueue::New;
|
||||||
|
c.set_due_date(6, 1);
|
||||||
|
assert_eq!(c.due, 7);
|
||||||
|
assert_eq!(c.interval, 2);
|
||||||
|
assert_eq!(c.queue, CardQueue::Review);
|
||||||
|
assert_eq!(c.original_due, 0);
|
||||||
|
assert_eq!(c.original_deck_id, DeckID(0));
|
||||||
|
|
||||||
|
// when relearning, a larger delay than the interval will win
|
||||||
|
c.ctype = CardType::Relearn;
|
||||||
|
c.original_due = c.due;
|
||||||
|
c.due = 12345678;
|
||||||
|
c.set_due_date(6, 10);
|
||||||
|
assert_eq!(c.due, 16);
|
||||||
|
assert_eq!(c.interval, 10);
|
||||||
|
|
||||||
|
// but a shorter delay will preserve the current interval
|
||||||
|
c.ctype = CardType::Relearn;
|
||||||
|
c.original_due = c.due;
|
||||||
|
c.due = 12345678;
|
||||||
|
c.set_due_date(6, 1);
|
||||||
|
assert_eq!(c.due, 7);
|
||||||
|
assert_eq!(c.interval, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user