Remove v1/v2 support from the backend (#2727)

* Remove v1/v2 support from deck list

* Remove v1/v2 support from most routines and show error

* Remove scheduler_version from preferences

* Fix formatting

* Remove v1/v2 conditionals from Python code

* Fix legacy importer

* Remove legacy hooks

* Add missing scheduler checks

* Remove V2 logic from deck options screen

* Remove the review_did_undo hook

* Restore ability to open old options with shift (dae)
This commit is contained in:
Abdo 2023-10-14 03:50:59 +03:00 committed by GitHub
parent e1e0f2e1bd
commit 5cde4b6941
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 230 additions and 775 deletions

@ -1 +1 @@
Subproject commit efd2e6edaf1e2b8e1d52c45ebd09d67ac035050f
Subproject commit 3d820846d847f8ed866971e3a4304c715325d01b

@ -1 +1 @@
Subproject commit 41663c8bd0f6386ee98de03ca2c3e668c4f6194d
Subproject commit ef4f4ffee68a3d3ebb2458b8b55bfdffa9a21c4b

View File

@ -44,6 +44,7 @@ message BackendError {
ANKIDROID_PANIC_ERROR = 19;
// Originated from and usually specific to the OS.
OS_ERROR = 20;
SCHEDULER_UPGRADE_REQUIRED = 21;
}
// error description, usually localized, suitable for displaying to the user

View File

@ -100,9 +100,6 @@ message Preferences {
NEW_FIRST = 2;
}
// read only; 1-3
uint32 scheduler_version = 1;
uint32 rollover = 2;
uint32 learn_ahead_secs = 3;
NewReviewMix new_review_mix = 4;

View File

@ -180,7 +180,6 @@ message DeckConfigsForUpdate {
CurrentDeck current_deck = 2;
DeckConfig defaults = 3;
bool schema_modified = 4;
bool v3_scheduler = 5;
// only applies to v3 scheduler
string card_state_customizer = 6;
// only applies to v3 scheduler

View File

@ -33,6 +33,7 @@ from .errors import (
InvalidInput,
NetworkError,
NotFoundError,
SchedulerUpgradeRequired,
SearchError,
SyncError,
SyncErrorKind,
@ -240,6 +241,9 @@ def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception:
elif val == kind.CUSTOM_STUDY_ERROR:
return CustomStudyError(err.message, help_page, context, backtrace)
elif val == kind.SCHEDULER_UPGRADE_REQUIRED:
return SchedulerUpgradeRequired(err.message, help_page, context, backtrace)
else:
# sadly we can't do exhaustiveness checking on protobuf enums
# assert_exhaustive(val)

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from typing import Any, Generator, Iterable, Literal, Sequence, Union, cast
from typing import Any, Generator, Iterable, Literal, Optional, Sequence, Union, cast
from anki import (
ankiweb_pb2,
@ -56,13 +56,12 @@ MediaSyncStatus = sync_pb2.MediaSyncStatusResponse
FsrsItem = scheduler_pb2.FsrsItem
FsrsReview = scheduler_pb2.FsrsReview
import copy
import os
import sys
import time
import traceback
import weakref
from dataclasses import dataclass, field
from dataclasses import dataclass
import anki.latex
from anki import hooks
@ -98,18 +97,12 @@ anki.latex.setup_hook()
SearchJoiner = Literal["AND", "OR"]
@dataclass
class LegacyReviewUndo:
card: Card
was_leech: bool
@dataclass
class LegacyCheckpoint:
name: str
LegacyUndoResult = Union[None, LegacyCheckpoint, LegacyReviewUndo]
LegacyUndoResult = Optional[LegacyCheckpoint]
@dataclass
@ -1075,9 +1068,7 @@ class Collection(DeprecatedNamesMixin):
if not self._undo:
return UndoStatus()
if isinstance(self._undo, _ReviewsUndo):
return UndoStatus(undo=self.tr.scheduling_review())
elif isinstance(self._undo, LegacyCheckpoint):
if isinstance(self._undo, LegacyCheckpoint):
return UndoStatus(undo=self._undo.name)
else:
assert_exhaustive(self._undo)
@ -1131,9 +1122,7 @@ class Collection(DeprecatedNamesMixin):
def undo_legacy(self) -> LegacyUndoResult:
"Returns None if the legacy undo queue is empty."
if isinstance(self._undo, _ReviewsUndo):
return self._undo_review()
elif isinstance(self._undo, LegacyCheckpoint):
if isinstance(self._undo, LegacyCheckpoint):
return self._undo_checkpoint()
elif self._undo is None:
return None
@ -1158,15 +1147,6 @@ class Collection(DeprecatedNamesMixin):
else:
return None
def save_card_review_undo_info(self, card: Card) -> None:
"Used by V1 and V2 schedulers to record state prior to review."
if not isinstance(self._undo, _ReviewsUndo):
self._undo = _ReviewsUndo()
was_leech = card.note().has_tag("leech")
entry = LegacyReviewUndo(card=copy.copy(card), was_leech=was_leech)
self._undo.entries.append(entry)
def _have_outstanding_checkpoint(self) -> bool:
self._check_backend_undo_status()
return isinstance(self._undo, LegacyCheckpoint)
@ -1184,59 +1164,8 @@ class Collection(DeprecatedNamesMixin):
if name:
self._undo = LegacyCheckpoint(name=name)
else:
# saving disables old checkpoint, but not review undo
if not isinstance(self._undo, _ReviewsUndo):
self.clear_python_undo()
def _undo_review(self) -> LegacyReviewUndo:
"Undo a v1/v2 review."
assert isinstance(self._undo, _ReviewsUndo)
entry = self._undo.entries.pop()
if not self._undo.entries:
self.clear_python_undo()
card = entry.card
# remove leech tag if it didn't have it before
if not entry.was_leech and card.note().has_tag("leech"):
card.note().remove_tag("leech")
card.note().flush()
# write old data
card.flush()
# and delete revlog entry if not previewing
conf = self.sched._cardConf(card)
previewing = conf["dyn"] and not conf["resched"]
if not previewing:
last = self.db.scalar(
"select id from revlog where cid = ? order by id desc limit 1",
card.id,
)
self.db.execute("delete from revlog where id = ?", last)
# restore any siblings
self.db.execute(
"update cards set queue=type,mod=?,usn=? where queue=-2 and nid=?",
int_time(),
self.usn(),
card.nid,
)
# update daily counts
idx = card.queue
if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW):
idx = QUEUE_TYPE_LRN
type = ("new", "lrn", "rev")[idx]
self.sched._updateStats(card, type, -1)
self.sched.reps -= 1
self._startReps -= 1
# and refresh the queues
self.sched.reset()
return entry
# DB maintenance
##########################################################################
@ -1444,7 +1373,6 @@ class Collection(DeprecatedNamesMixin):
Collection.register_deprecated_aliases(
clearUndo=Collection.clear_python_undo,
markReview=Collection.save_card_review_undo_info,
findReplace=Collection.find_and_replace,
remCards=Collection.remove_cards_and_orphaned_notes,
)
@ -1453,13 +1381,7 @@ Collection.register_deprecated_aliases(
# legacy name
_Collection = Collection
@dataclass
class _ReviewsUndo:
entries: list[LegacyReviewUndo] = field(default_factory=list)
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
_UndoInfo = Union[LegacyCheckpoint, None]
def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit:

View File

@ -119,6 +119,10 @@ class SearchError(BackendError):
pass
class SchedulerUpgradeRequired(BackendError):
pass
class AbortSchemaModification(AnkiException):
pass

View File

@ -197,9 +197,6 @@ class AnkiExporter(Exporter):
return []
def exportInto(self, path: str) -> None:
# sched info+v2 scheduler not compatible w/ older clients
self._v2sched = self.col.sched_ver() != 1 and self.includeSched
# create a new collection at the target
try:
os.unlink(path)
@ -352,13 +349,10 @@ class AnkiPackageExporter(AnkiExporter):
# export into the anki2 file
colfile = path.replace(".apkg", ".anki2")
AnkiExporter.exportInto(self, colfile)
if not self._v2sched:
z.write(colfile, "collection.anki2")
else:
# prevent older clients from accessing
# pylint: disable=unreachable
self._addDummyCollection(z)
z.write(colfile, "collection.anki21")
# prevent older clients from accessing
# pylint: disable=unreachable
self._addDummyCollection(z)
z.write(colfile, "collection.anki21")
# and media
self.prepareMedia()

View File

@ -60,7 +60,7 @@ class Anki2Importer(Importer):
self.dst = self.col
self.src = Collection(self.file)
if not self._importing_v2 and self.col.sched_ver() != 1:
if not self._importing_v2:
# any scheduling included?
if self.src.db.scalar("select 1 from cards where queue != 0 limit 1"):
self.source_needs_upgrade = True

View File

@ -5,7 +5,6 @@
from __future__ import annotations
import datetime
import json
import random
import time
@ -691,8 +690,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
[13, 3],
[14, 4],
]
if self.col.sched_ver() != 1:
ticks.insert(3, [4, 4])
ticks.insert(3, [4, 4])
txt = self._title(
"Answer Buttons", "The number of times you have pressed each button."
)
@ -751,10 +749,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
lim = "where " + " and ".join(lims)
else:
lim = ""
if self.col.sched_ver() == 1:
ease4repl = "3"
else:
ease4repl = "ease"
ease4repl = "ease"
return self.col.db.all(
f"""
select (case
@ -841,11 +836,7 @@ order by thetype, ease"""
lim = self._revlogLimit()
if lim:
lim = " and " + lim
if self.col.sched_ver() == 1:
sd = datetime.datetime.fromtimestamp(self.col.crt)
rolloverHour = sd.hour
else:
rolloverHour = self.col.conf.get("rollover", 4)
rolloverHour = self.col.conf.get("rollover", 4)
pd = self._periodDays()
if pd:
lim += " and id > %d" % ((self.col.sched.day_cutoff - (86400 * pd)) * 1000)

View File

@ -395,13 +395,7 @@ def test_reviews():
c = copy.copy(cardcopy)
c.lapses = 7
c.flush()
# setup hook
hooked = []
def onLeech(card):
hooked.append(1)
hooks.card_did_leech.append(onLeech)
col.sched.answerCard(c, 1)
assert c.queue == QUEUE_TYPE_SUSPENDED
c.load()

View File

@ -18,12 +18,6 @@ from hookslib import Hook, write_file
######################################################################
hooks = [
Hook(
name="card_did_leech",
args=["card: Card"],
legacy_hook="leech",
doc="Called by v1/v2 scheduler when a card is marked as a leech.",
),
Hook(name="card_odue_was_invalid"),
Hook(name="schema_will_change", args=["proceed: bool"], return_type="bool"),
Hook(
@ -98,24 +92,6 @@ hooks = [
],
doc="Can modify the resulting text after rendering completes.",
),
Hook(
name="schedv2_did_answer_review_card",
args=["card: anki.cards.Card", "ease: int", "early: bool"],
),
Hook(
name="scheduler_new_limit_for_single_deck",
args=["count: int", "deck: anki.decks.DeckDict"],
return_type="int",
doc="""Allows changing the number of new card for this deck (without
considering descendants).""",
),
Hook(
name="scheduler_review_limit_for_single_deck",
args=["count: int", "deck: anki.decks.DeckDict"],
return_type="int",
doc="""Allows changing the number of rev card for this deck (without
considering descendants).""",
),
Hook(
name="importing_importers",
args=["importers: list[tuple[str, Any]]"],

View File

@ -729,7 +729,7 @@ class Browser(QMainWindow):
def createFilteredDeck(self) -> None:
search = self.current_search()
if self.mw.col.sched_ver() != 1 and KeyboardModifiersPressed().alt:
if KeyboardModifiersPressed().alt:
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search_2=search)
else:
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search=search)

View File

@ -219,9 +219,6 @@ class DeckConf(QDialog):
f.revplim.setText(self.parentLimText("rev"))
f.buryRev.setChecked(c.get("bury", True))
f.hardFactor.setValue(int(c.get("hardFactor", 1.2) * 100))
if self.mw.col.sched_ver() == 1:
f.hardFactor.setVisible(False)
f.hardFactorLabel.setVisible(False)
# lapse
c = self.conf["lapse"]
f.lapSteps.setText(self.listToUser(c["delays"]))

View File

@ -106,7 +106,7 @@ def display_options_for_deck_id(deck_id: DeckId) -> None:
def display_options_for_deck(deck: DeckDict) -> None:
if not deck["dyn"]:
if KeyboardModifiersPressed().shift or aqt.mw.col.sched_ver() == 1:
if KeyboardModifiersPressed().shift or not aqt.mw.col.v3_scheduler():
deck_legacy = aqt.mw.col.decks.get(DeckId(deck["id"]))
aqt.deckconf.DeckConf(aqt.mw, deck_legacy)
else:

View File

@ -98,8 +98,6 @@ class FilteredDeckConfigDialog(QDialog):
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.FILTERED_DECK)
)
if self.col.sched_ver() == 1:
self.form.secondFilter.setVisible(False)
restoreGeom(self, self.GEOMETRY_KEY)
def load_deck_and_show(self, deck: FilteredDeckForUpdate) -> None:
@ -132,13 +130,8 @@ class FilteredDeckConfigDialog(QDialog):
form.order.setCurrentIndex(term1.order)
form.limit.setValue(term1.limit)
if self.col.sched_ver() == 1:
if config.delays:
form.steps.setText(self.listToUser(list(config.delays)))
form.stepsOn.setChecked(True)
else:
form.steps.setVisible(False)
form.stepsOn.setVisible(False)
form.steps.setVisible(False)
form.stepsOn.setVisible(False)
form.previewDelay.setValue(config.preview_delay)
@ -209,7 +202,6 @@ class FilteredDeckConfigDialog(QDialog):
implicit_filters = (
SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED),
SearchNode(card_state=SearchNode.CARD_STATE_BURIED),
*self._learning_search_node(),
*self._filtered_search_node(),
)
manual_filter = self.col.group_searches(*manual_filters, joiner="OR")
@ -226,21 +218,6 @@ class FilteredDeckConfigDialog(QDialog):
return (self.form.search_2.text(),)
return ()
def _learning_search_node(self) -> tuple[SearchNode, ...]:
"""Return a search node that matches learning cards if the old scheduler is enabled.
If it's a rebuild, exclude cards from this filtered deck as those will be reset.
"""
if self.col.sched_ver() == 1:
if self.deck.id:
return (
self.col.group_searches(
SearchNode(card_state=SearchNode.CARD_STATE_LEARN),
SearchNode(negated=SearchNode(deck=self.deck.name)),
),
)
return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),)
return ()
def _filtered_search_node(self) -> tuple[SearchNode]:
"""Return a search node that matches cards in filtered decks, if applicable excluding those
in the deck being rebuild."""
@ -254,9 +231,7 @@ class FilteredDeckConfigDialog(QDialog):
return (SearchNode(deck="filtered"),)
def _onReschedToggled(self, _state: int) -> None:
self.form.previewDelayWidget.setVisible(
not self.form.resched.isChecked() and self.col.sched_ver() > 1
)
self.form.previewDelayWidget.setVisible(not self.form.resched.isChecked())
def _update_deck(self) -> bool:
"""Update our stored deck with the details from the GUI.
@ -269,11 +244,6 @@ class FilteredDeckConfigDialog(QDialog):
config.reschedule = form.resched.isChecked()
del config.delays[:]
if self.col.sched_ver() == 1 and form.stepsOn.isChecked():
if (delays := self.userToList(form.steps)) is None:
return False
config.delays.extend(delays)
terms = [
FilteredDeckConfig.SearchTerm(
search=form.search.text(),

View File

@ -3,13 +3,7 @@
from __future__ import annotations
from anki.collection import (
LegacyCheckpoint,
LegacyReviewUndo,
OpChanges,
OpChangesAfterUndo,
Preferences,
)
from anki.collection import LegacyCheckpoint, OpChanges, OpChangesAfterUndo, Preferences
from anki.errors import UndoEmpty
from anki.types import assert_exhaustive
from aqt import gui_hooks
@ -27,8 +21,7 @@ def undo(*, parent: QWidget) -> None:
def on_failure(exc: Exception) -> None:
if isinstance(exc, UndoEmpty):
# backend has no undo, but there may be a checkpoint
# or v1/v2 review waiting
# backend has no undo, but there may be a checkpoint waiting
_legacy_undo(parent=parent)
else:
showWarning(str(exc), parent=parent)
@ -53,9 +46,6 @@ def _legacy_undo(*, parent: QWidget) -> None:
assert mw
assert mw.col
reviewing = mw.state == "review"
just_refresh_reviewer = False
result = mw.col.undo_legacy()
if result is None:
@ -64,19 +54,6 @@ def _legacy_undo(*, parent: QWidget) -> None:
mw.update_undo_actions()
return
elif isinstance(result, LegacyReviewUndo):
name = tr.scheduling_review()
if reviewing:
# push the undone card to the top of the queue
cid = result.card.id
card = mw.col.get_card(cid)
mw.reviewer.cardQueue.append(card)
gui_hooks.review_did_undo(cid)
just_refresh_reviewer = True
elif isinstance(result, LegacyCheckpoint):
name = result.name
@ -84,11 +61,8 @@ def _legacy_undo(*, parent: QWidget) -> None:
assert_exhaustive(result)
assert False
if just_refresh_reviewer:
mw.reviewer.nextCard()
else:
# full queue+gui reset required
mw.reset()
# full queue+gui reset required
mw.reset()
tooltip(tr.undo_action_undone(action=name), parent=parent)
gui_hooks.state_did_revert(name)

View File

@ -150,25 +150,24 @@ class Overview:
def on_unbury(self) -> None:
mode = UnburyDeck.Mode.ALL
if self.mw.col.sched_ver() != 1:
info = self.mw.col.sched.congratulations_info()
if info.have_sched_buried and info.have_user_buried:
opts = [
tr.studying_manually_buried_cards(),
tr.studying_buried_siblings(),
tr.studying_all_buried_cards(),
tr.actions_cancel(),
]
info = self.mw.col.sched.congratulations_info()
if info.have_sched_buried and info.have_user_buried:
opts = [
tr.studying_manually_buried_cards(),
tr.studying_buried_siblings(),
tr.studying_all_buried_cards(),
tr.actions_cancel(),
]
diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts)
diag.setDefault(0)
ret = diag.run()
if ret == opts[0]:
mode = UnburyDeck.Mode.USER_ONLY
elif ret == opts[1]:
mode = UnburyDeck.Mode.SCHED_ONLY
elif ret == opts[3]:
return
diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts)
diag.setDefault(0)
ret = diag.run()
if ret == opts[0]:
mode = UnburyDeck.Mode.USER_ONLY
elif ret == opts[1]:
mode = UnburyDeck.Mode.SCHED_ONLY
elif ret == opts[3]:
return
unbury_deck(
parent=self.mw, deck_id=self.mw.col.decks.get_current_id(), mode=mode

View File

@ -14,7 +14,6 @@ from typing import Any, Literal, Match, Sequence, cast
import aqt
import aqt.browser
import aqt.operations
from anki import hooks
from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount
from anki.scheduler.base import ScheduleCardsAsNew
@ -135,9 +134,7 @@ class Reviewer:
self.mw = mw
self.web = mw.web
self.card: Card | None = None
self.cardQueue: list[Card] = []
self.previous_card: Card | None = None
self.hadCardQueue = False
self._answeredIds: list[CardId] = []
self._recordedAudio: str | None = None
self.typeCorrect: str = None # web init happens before this is set
@ -150,7 +147,6 @@ class Reviewer:
self._previous_card_info = PreviousReviewerCardInfo(self.mw)
self._states_mutated = True
self._reps: int = None
hooks.card_did_leech.append(self.onLeech)
def show(self) -> None:
if self.mw.col.sched_ver() == 1 or not self.mw.col.v3_scheduler():
@ -229,11 +225,7 @@ class Reviewer:
self.previous_card = self.card
self.card = None
self._v3 = None
if self.mw.col.sched.version < 3:
self._get_next_v1_v2_card()
else:
self._get_next_v3_card()
self._get_next_v3_card()
self._previous_card_info.set_card(self.previous_card)
self._card_info.set_card(self.card)
@ -247,21 +239,6 @@ class Reviewer:
self._showQuestion()
def _get_next_v1_v2_card(self) -> None:
if self.cardQueue:
# undone/edited cards to show
card = self.cardQueue.pop()
card.start_timer()
self.hadCardQueue = True
else:
if self.hadCardQueue:
# the undone/edited cards may be sitting in the regular queue;
# need to reset
self.mw.col.reset()
self.hadCardQueue = False
card = self.mw.col.sched.getCard()
self.card = card
def _get_next_v3_card(self) -> None:
assert isinstance(self.mw.col.sched, V3Scheduler)
output = self.mw.col.sched.get_queued_cards()
@ -271,22 +248,17 @@ class Reviewer:
self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)
self.card.start_timer()
def get_scheduling_states(self) -> SchedulingStates | None:
if v3 := self._v3:
return v3.states
return None
def get_scheduling_states(self) -> SchedulingStates:
return self._v3.states
def get_scheduling_context(self) -> SchedulingContext | None:
if v3 := self._v3:
return v3.context
return None
def get_scheduling_context(self) -> SchedulingContext:
return self._v3.context
def set_scheduling_states(self, request: SetSchedulingStatesRequest) -> None:
if request.key != self._state_mutation_key:
return
if v3 := self._v3:
v3.states = request.states
self._v3.states = request.states
def _run_state_mutation_hook(self) -> None:
def on_eval(result: Any) -> None:
@ -294,7 +266,7 @@ class Reviewer:
# eval failed, usually a syntax error
self._states_mutated = True
if self._v3 and (js := self._state_mutation_js):
if js := self._state_mutation_js:
self._states_mutated = False
self.web.evalWithCallback(
RUN_STATE_MUTATION.format(key=self._state_mutation_key, js=js),
@ -450,27 +422,24 @@ class Reviewer:
if not proceed:
return
if (v3 := self._v3) and (sched := cast(V3Scheduler, self.mw.col.sched)):
answer = sched.build_answer(
card=self.card,
states=v3.states,
rating=v3.rating_from_ease(ease),
)
sched = cast(V3Scheduler, self.mw.col.sched)
answer = sched.build_answer(
card=self.card,
states=self._v3.states,
rating=self._v3.rating_from_ease(ease),
)
def after_answer(changes: OpChanges) -> None:
if gui_hooks.reviewer_did_answer_card.count() > 0:
self.card.load()
self._after_answering(ease)
if sched.state_is_leech(answer.new_state):
self.onLeech()
self.state = "transition"
answer_card(parent=self.mw, answer=answer).success(
after_answer
).run_in_background(initiator=self)
else:
self.mw.col.sched.answerCard(self.card, ease)
def after_answer(changes: OpChanges) -> None:
if gui_hooks.reviewer_did_answer_card.count() > 0:
self.card.load()
self._after_answering(ease)
if sched.state_is_leech(answer.new_state):
self.onLeech()
self.state = "transition"
answer_card(parent=self.mw, answer=answer).success(
after_answer
).run_in_background(initiator=self)
def _after_answering(self, ease: Literal[1, 2, 3, 4]) -> None:
gui_hooks.reviewer_did_answer_card(self, self.card, ease)
@ -792,11 +761,8 @@ timerStopped = false;
def _answerButtons(self) -> str:
default = self._defaultEase()
if v3 := self._v3:
assert isinstance(self.mw.col.sched, V3Scheduler)
labels = self.mw.col.sched.describe_next_states(v3.states)
else:
labels = None
assert isinstance(self.mw.col.sched, V3Scheduler)
labels = self.mw.col.sched.describe_next_states(self._v3.states)
def but(i: int, label: str) -> str:
if i == default:

View File

@ -823,7 +823,6 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest)
legacy_hook="colLoading",
),
Hook(name="undo_state_did_change", args=["info: UndoActionsInfo"]),
Hook(name="review_did_undo", args=["card_id: int"], legacy_hook="revertedCard"),
Hook(
name="style_did_init",
args=["style: str"],

View File

@ -46,6 +46,7 @@ impl AnkiError {
| AnkiError::FsrsInsufficientData => Kind::InvalidInput,
#[cfg(windows)]
AnkiError::WindowsError { .. } => Kind::OsError,
AnkiError::SchedulerUpgradeRequired => Kind::SchedulerUpgradeRequired,
};
anki_proto::backend::BackendError {

View File

@ -184,8 +184,8 @@ impl Card {
}
/// Caller must ensure provided deck exists and is not filtered.
fn set_deck(&mut self, deck: DeckId, sched: SchedulerVersion) {
self.remove_from_filtered_deck_restoring_queue(sched);
fn set_deck(&mut self, deck: DeckId) {
self.remove_from_filtered_deck_restoring_queue();
self.deck_id = deck;
}
@ -342,13 +342,16 @@ impl Collection {
}
pub fn set_deck(&mut self, cards: &[CardId], deck_id: DeckId) -> Result<OpOutput<usize>> {
let sched = self.scheduler_version();
if sched == SchedulerVersion::V1 {
return Err(AnkiError::SchedulerUpgradeRequired);
}
let deck = self.get_deck(deck_id)?.or_not_found(deck_id)?;
let config_id = deck.config_id().ok_or(AnkiError::FilteredDeckError {
source: FilteredDeckError::CanNotMoveCardsInto,
})?;
let config = self.get_deck_config(config_id, true)?.unwrap();
let mut steps_adjuster = RemainingStepsAdjuster::new(&config);
let sched = self.scheduler_version();
let usn = self.usn()?;
self.transact(Op::SetCardDeck, |col| {
let mut count = 0;
@ -359,7 +362,7 @@ impl Collection {
count += 1;
let original = card.clone();
steps_adjuster.adjust_remaining_steps(col, &mut card)?;
card.set_deck(deck_id, sched);
card.set_deck(deck_id);
col.update_card_inner(&mut card, original, usn)?;
}
Ok(count)

View File

@ -50,7 +50,6 @@ impl Collection {
.storage
.get_collection_timestamps()?
.schema_changed_since_sync(),
v3_scheduler: self.get_config_bool(BoolKey::Sched2021),
card_state_customizer: self.get_config_string(StringKey::CardStateCustomizer),
new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit),
fsrs: self.get_config_bool(BoolKey::Fsrs),

View File

@ -35,8 +35,7 @@ impl Collection {
days_elapsed: u32,
learn_cutoff: u32,
) -> Result<HashMap<DeckId, DueCounts>> {
self.storage
.due_counts(self.scheduler_version(), days_elapsed, learn_cutoff)
self.storage.due_counts(days_elapsed, learn_cutoff)
}
pub(crate) fn counts_for_deck_today(

View File

@ -62,7 +62,6 @@ impl RemainingLimits {
deck: &Deck,
config: Option<&DeckConfig>,
today: u32,
v3: bool,
new_cards_ignore_review_limit: bool,
) -> Self {
if let Ok(normal) = deck.normal() {
@ -70,7 +69,6 @@ impl RemainingLimits {
return Self::new_for_normal_deck(
deck,
today,
v3,
new_cards_ignore_review_limit,
normal,
config,
@ -83,28 +81,11 @@ impl RemainingLimits {
fn new_for_normal_deck(
deck: &Deck,
today: u32,
v3: bool,
new_cards_ignore_review_limit: bool,
normal: &NormalDeck,
config: &DeckConfig,
) -> RemainingLimits {
if v3 {
Self::new_for_normal_deck_v3(deck, today, new_cards_ignore_review_limit, normal, config)
} else {
Self::new_for_normal_deck_v2(deck, today, config)
}
}
fn new_for_normal_deck_v2(deck: &Deck, today: u32, config: &DeckConfig) -> RemainingLimits {
let review_limit = config.inner.reviews_per_day;
let new_limit = config.inner.new_per_day;
let (new_today_count, review_today_count) = deck.new_rev_counts(today);
Self {
review: (review_limit as i32 - review_today_count).max(0) as u32,
new: (new_limit as i32 - new_today_count).max(0) as u32,
cap_new_to_review: false,
}
Self::new_for_normal_deck_v3(deck, today, new_cards_ignore_review_limit, normal, config)
}
fn new_for_normal_deck_v3(
@ -189,7 +170,6 @@ pub(crate) fn remaining_limits_map<'a>(
decks: impl Iterator<Item = &'a Deck>,
config: &'a HashMap<DeckConfigId, DeckConfig>,
today: u32,
v3: bool,
new_cards_ignore_review_limit: bool,
) -> HashMap<DeckId, RemainingLimits> {
decks
@ -200,7 +180,6 @@ pub(crate) fn remaining_limits_map<'a>(
deck,
deck.config_id().and_then(|id| config.get(&id)),
today,
v3,
new_cards_ignore_review_limit,
),
)
@ -231,7 +210,6 @@ impl NodeLimits {
deck,
deck.config_id().and_then(|id| config.get(&id)),
today,
true,
new_cards_ignore_review_limit,
),
}

View File

@ -14,7 +14,6 @@ use unicase::UniCase;
use super::limits::remaining_limits_map;
use super::limits::RemainingLimits;
use super::DueCounts;
use crate::config::SchedulerVersion;
use crate::ops::OpOutput;
use crate::prelude::*;
use crate::undo::Op;
@ -100,66 +99,6 @@ fn add_counts(node: &mut DeckTreeNode, counts: &HashMap<DeckId, DueCounts>) {
}
}
/// Apply parent limits to children, and add child counts to parents.
fn sum_counts_and_apply_limits_v1(
node: &mut DeckTreeNode,
limits: &HashMap<DeckId, RemainingLimits>,
parent_limits: RemainingLimits,
) {
let mut remaining = limits
.get(&DeckId(node.deck_id))
.copied()
.unwrap_or_default();
remaining.cap_to(parent_limits);
// apply our limit to children and tally their counts
let mut child_new_total = 0;
let mut child_rev_total = 0;
for child in &mut node.children {
sum_counts_and_apply_limits_v1(child, limits, remaining);
child_new_total += child.new_count;
child_rev_total += child.review_count;
// no limit on learning cards
node.learn_count += child.learn_count;
}
// add child counts to our count, capped to remaining limit
node.new_count = (node.new_count + child_new_total).min(remaining.new);
node.review_count = (node.review_count + child_rev_total).min(remaining.review);
}
/// Apply parent new limits to children, and add child counts to parents. Unlike
/// v1, reviews are not capped by their parents, and we
/// return the uncapped review amount to add to the parent.
fn sum_counts_and_apply_limits_v2(
node: &mut DeckTreeNode,
limits: &HashMap<DeckId, RemainingLimits>,
parent_limits: RemainingLimits,
) -> u32 {
let original_rev_count = node.review_count;
let mut remaining = limits
.get(&DeckId(node.deck_id))
.copied()
.unwrap_or_default();
remaining.new = remaining.new.min(parent_limits.new);
// apply our limit to children and tally their counts
let mut child_new_total = 0;
let mut child_rev_total = 0;
for child in &mut node.children {
child_rev_total += sum_counts_and_apply_limits_v2(child, limits, remaining);
child_new_total += child.new_count;
// no limit on learning cards
node.learn_count += child.learn_count;
}
// add child counts to our count, capped to remaining limit
node.new_count = (node.new_count + child_new_total).min(remaining.new);
node.review_count = (node.review_count + child_rev_total).min(remaining.review);
original_rev_count + child_rev_total
}
/// A temporary container used during count summation and limit application.
#[derive(Default, Clone)]
struct NodeCountsV3 {
@ -325,8 +264,6 @@ impl Collection {
let timing_at_stamp = self.timing_for_timestamp(timestamp)?;
let days_elapsed = timing_at_stamp.days_elapsed;
let learn_cutoff = (timestamp.0 as u32) + self.learn_ahead_secs();
let sched_ver = self.scheduler_version();
let v3 = self.get_config_bool(BoolKey::Sched2021);
let new_cards_ignore_review_limit =
self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit);
let counts = self.due_counts(days_elapsed, learn_cutoff)?;
@ -336,18 +273,9 @@ impl Collection {
decks_map.values(),
&dconf,
days_elapsed,
v3,
new_cards_ignore_review_limit,
);
if sched_ver == SchedulerVersion::V2 {
if v3 {
sum_counts_and_apply_limits_v3(&mut tree, &limits);
} else {
sum_counts_and_apply_limits_v2(&mut tree, &limits, RemainingLimits::default());
}
} else {
sum_counts_and_apply_limits_v1(&mut tree, &limits, RemainingLimits::default());
}
sum_counts_and_apply_limits_v3(&mut tree, &limits);
}
Ok(tree)

View File

@ -115,6 +115,7 @@ pub enum AnkiError {
InvalidServiceIndex,
FsrsWeightsInvalid,
FsrsInsufficientData,
SchedulerUpgradeRequired,
}
// error helpers
@ -168,6 +169,9 @@ impl AnkiError {
AnkiError::NotFound { source } => source.message(tr),
AnkiError::FsrsInsufficientData => tr.deck_config_not_enough_history().into(),
AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_weights().into(),
AnkiError::SchedulerUpgradeRequired => {
tr.scheduling_update_required().replace("V2", "v3")
}
#[cfg(windows)]
AnkiError::WindowsError { source } => format!("{source:?}"),
}

View File

@ -89,6 +89,9 @@ impl Context<'_> {
remapped_templates,
imported_decks,
)?;
if ctx.scheduler_version == SchedulerVersion::V1 {
return Err(AnkiError::SchedulerUpgradeRequired);
}
ctx.import_cards(mem::take(&mut self.data.cards), keep_filtered)?;
ctx.import_revlog(mem::take(&mut self.data.revlog))
}
@ -136,7 +139,7 @@ impl CardContext<'_> {
self.remap_template_index(card);
card.shift_collection_relative_dates(self.collection_delta);
if !keep_filtered {
card.maybe_remove_from_filtered_deck(self.scheduler_version);
card.maybe_remove_from_filtered_deck();
}
let old_id = self.uniquify_card_id(card);
@ -196,11 +199,11 @@ impl Card {
self.ctype == CardType::Review
}
fn maybe_remove_from_filtered_deck(&mut self, version: SchedulerVersion) {
fn maybe_remove_from_filtered_deck(&mut self) {
if self.is_filtered() {
// instead of moving between decks, the deck is converted to a regular one
self.original_deck_id = self.deck_id;
self.remove_from_filtered_deck_restoring_queue(version);
self.remove_from_filtered_deck_restoring_queue();
}
}
}

View File

@ -48,16 +48,6 @@ impl Collection {
pub fn get_scheduling_preferences(&self) -> Result<Scheduling> {
Ok(Scheduling {
scheduler_version: match self.scheduler_version() {
crate::config::SchedulerVersion::V1 => 1,
crate::config::SchedulerVersion::V2 => {
if self.get_config_bool(BoolKey::Sched2021) {
3
} else {
2
}
}
},
rollover: self.rollover_for_current_scheduler()? as u32,
learn_ahead_secs: self.learn_ahead_secs(),
new_review_mix: match self.get_new_review_mix() {

View File

@ -4,7 +4,6 @@
use super::CardStateUpdater;
use super::RevlogEntryPartial;
use crate::card::CardQueue;
use crate::config::SchedulerVersion;
use crate::scheduler::states::CardState;
use crate::scheduler::states::IntervalKind;
use crate::scheduler::states::PreviewState;
@ -17,8 +16,7 @@ impl CardStateUpdater {
) -> RevlogEntryPartial {
let revlog = RevlogEntryPartial::new(current, next.into(), 0.0, self.secs_until_rollover());
if next.finished {
self.card
.remove_from_filtered_deck_restoring_queue(SchedulerVersion::V2);
self.card.remove_from_filtered_deck_restoring_queue();
return revlog;
}

View File

@ -90,17 +90,13 @@ impl Collection {
let mut count = 0;
let usn = self.usn()?;
let sched = self.scheduler_version();
if sched == SchedulerVersion::V1 {
return Err(AnkiError::SchedulerUpgradeRequired);
}
let desired_queue = match mode {
BuryOrSuspendMode::Suspend => CardQueue::Suspended,
BuryOrSuspendMode::BurySched => CardQueue::SchedBuried,
BuryOrSuspendMode::BuryUser => {
if sched == SchedulerVersion::V1 {
// v1 scheduler only had one bury type
CardQueue::SchedBuried
} else {
CardQueue::UserBuried
}
}
BuryOrSuspendMode::BuryUser => CardQueue::UserBuried,
};
for original in cards {
@ -108,10 +104,6 @@ impl Collection {
if card.queue != desired_queue {
// do not bury suspended cards as that would unsuspend them
if card.queue != CardQueue::Suspended {
if sched == SchedulerVersion::V1 {
card.remove_from_filtered_deck_restoring_queue(sched);
card.remove_from_learning();
}
card.queue = desired_queue;
count += 1;
self.update_card_inner(&mut card, original, usn)?;

View File

@ -4,7 +4,6 @@
use super::DeckFilterContext;
use crate::card::CardQueue;
use crate::card::CardType;
use crate::config::SchedulerVersion;
use crate::prelude::*;
impl Card {
@ -37,26 +36,12 @@ impl Card {
self.original_due = self.due;
if ctx.scheduler == SchedulerVersion::V1 {
if self.ctype == CardType::Review && self.due <= ctx.today as i32 {
// review cards that are due are left in the review queue
} else {
// new + non-due go into new queue
self.queue = CardQueue::New;
}
if self.due != 0 {
self.due = position;
}
} else {
// if rescheduling is disabled, all cards go in the review queue
if !ctx.config.reschedule {
self.queue = CardQueue::Review;
}
// fixme: can we unify this with v1 scheduler in the future?
// https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745
if self.due > 0 {
self.due = position;
}
// if rescheduling is disabled, all cards go in the review queue
if !ctx.config.reschedule {
self.queue = CardQueue::Review;
}
if self.due > 0 {
self.due = position;
}
}
@ -75,7 +60,7 @@ impl Card {
self.original_deck_id.or(self.deck_id)
}
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self) {
if self.original_deck_id.0 == 0 {
// not in a filtered deck
return;
@ -84,33 +69,13 @@ impl Card {
self.deck_id = self.original_deck_id;
self.original_deck_id.0 = 0;
match sched {
SchedulerVersion::V1 => {
self.due = self.original_due;
self.queue = match self.ctype {
CardType::New => CardQueue::New,
CardType::Learn => CardQueue::New,
CardType::Review => CardQueue::Review,
// not applicable in v1, should not happen
CardType::Relearn => {
println!("did not expect relearn type in v1 for card {}", self.id);
CardQueue::New
}
};
if self.ctype == CardType::Learn {
self.ctype = CardType::New;
}
}
SchedulerVersion::V2 => {
// original_due is cleared if card answered in filtered deck
if self.original_due != 0 {
self.due = self.original_due;
}
// original_due is cleared if card answered in filtered deck
if self.original_due != 0 {
self.due = self.original_due;
}
if (self.queue as i8) >= 0 {
self.restore_queue_from_type();
}
}
if (self.queue as i8) >= 0 {
self.restore_queue_from_type();
}
self.original_due = 0;

View File

@ -26,7 +26,6 @@ pub struct FilteredDeckForUpdate {
pub(crate) struct DeckFilterContext<'a> {
pub target_deck: DeckId,
pub config: &'a FilteredDeck,
pub scheduler: SchedulerVersion,
pub usn: Usn,
pub today: u32,
}
@ -84,12 +83,11 @@ impl Collection {
// Unlike the old Python code, this also marks the cards as modified.
fn return_cards_to_home_deck(&mut self, cids: &[CardId]) -> Result<()> {
let sched = self.scheduler_version();
let usn = self.usn()?;
for cid in cids {
if let Some(mut card) = self.storage.get_card(*cid)? {
let original = card.clone();
card.remove_from_filtered_deck_restoring_queue(sched);
card.remove_from_filtered_deck_restoring_queue();
self.update_card_inner(&mut card, original, usn)?;
}
}
@ -99,12 +97,7 @@ impl Collection {
fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<usize> {
let start = -100_000;
let mut position = start;
let limit = if ctx.scheduler == SchedulerVersion::V1 {
1
} else {
2
};
for term in ctx.config.search_terms.iter().take(limit) {
for term in ctx.config.search_terms.iter().take(2) {
position = self.move_cards_matching_term(&ctx, term, position)?;
}
@ -120,16 +113,11 @@ impl Collection {
mut position: i32,
) -> Result<i32> {
let search = format!(
"{} -is:suspended -is:buried -deck:filtered {}",
"{} -is:suspended -is:buried -deck:filtered",
if term.search.trim().is_empty() {
"".to_string()
} else {
format!("({})", term.search)
},
if ctx.scheduler == SchedulerVersion::V1 {
"-is:learn"
} else {
""
}
);
let order = order_and_limit_for_search(term, ctx.today);
@ -189,11 +177,14 @@ impl Collection {
}
fn rebuild_filtered_deck_inner(&mut self, deck: &Deck, usn: Usn) -> Result<usize> {
if self.scheduler_version() == SchedulerVersion::V1 {
return Err(AnkiError::SchedulerUpgradeRequired);
}
let config = deck.filtered()?;
let ctx = DeckFilterContext {
target_deck: deck.id,
config,
scheduler: self.scheduler_version(),
usn,
today: self.timing_today()?.days_elapsed,
};

View File

@ -1,35 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::card::Card;
use crate::card::CardQueue;
use crate::card::CardType;
use crate::deckconfig::INITIAL_EASE_FACTOR_THOUSANDS;
impl Card {
/// Remove the card from the (re)learning queue.
/// This will reset cards in learning.
/// Only used in the V1 scheduler.
/// Unlike the legacy Python code, this sets the due# to 0 instead of
/// one past the previous max due number.
pub(crate) fn remove_from_learning(&mut self) {
if !matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn) {
return;
}
if self.ctype == CardType::Review {
// reviews are removed from relearning
self.due = self.original_due;
self.original_due = 0;
self.queue = CardQueue::Review;
} else {
// other cards are reset to new
self.ctype = CardType::New;
self.queue = CardQueue::New;
self.interval = 0;
self.due = 0;
self.original_due = 0;
self.ease_factor = INITIAL_EASE_FACTOR_THOUSANDS;
}
}
}

View File

@ -11,7 +11,6 @@ pub mod bury_and_suspend;
pub(crate) mod congrats;
pub(crate) mod filtered;
pub mod fsrs;
mod learning;
pub mod new;
pub(crate) mod queue;
mod reviews;
@ -24,8 +23,6 @@ mod upgrade;
use chrono::FixedOffset;
pub use reviews::parse_due_date_str;
use timing::sched_timing_today;
use timing::v1_creation_date_adjusted_to_hour;
use timing::v1_rollover_from_creation_stamp;
use timing::SchedTimingToday;
#[derive(Debug, Clone, Copy)]
@ -118,16 +115,14 @@ impl Collection {
pub fn rollover_for_current_scheduler(&self) -> Result<u8> {
match self.scheduler_version() {
SchedulerVersion::V1 => v1_rollover_from_creation_stamp(self.storage.creation_stamp()?),
SchedulerVersion::V1 => Err(AnkiError::SchedulerUpgradeRequired),
SchedulerVersion::V2 => Ok(self.get_v2_rollover().unwrap_or(4)),
}
}
pub(crate) fn set_rollover_for_current_scheduler(&mut self, hour: u8) -> Result<()> {
match self.scheduler_version() {
SchedulerVersion::V1 => self.set_creation_stamp(TimestampSecs(
v1_creation_date_adjusted_to_hour(self.storage.creation_stamp()?, hour)?,
)),
SchedulerVersion::V1 => Err(AnkiError::SchedulerUpgradeRequired),
SchedulerVersion::V2 => self.set_v2_rollover(hour as u32),
}
}

View File

@ -63,8 +63,8 @@ impl Card {
}
/// If the card is new, change its position, and return true.
fn set_new_position(&mut self, position: u32, v2: bool) -> bool {
if v2 && self.ctype == CardType::New {
fn set_new_position(&mut self, position: u32) -> bool {
if self.ctype == CardType::New {
if self.is_filtered() {
self.original_due = position as i32;
} else {
@ -234,16 +234,18 @@ impl Collection {
shift: bool,
usn: Usn,
) -> Result<usize> {
let v2 = self.scheduler_version() != SchedulerVersion::V1;
if self.scheduler_version() == SchedulerVersion::V1 {
return Err(AnkiError::SchedulerUpgradeRequired);
}
if shift {
self.shift_existing_cards(starting_from, step * cids.len() as u32, usn, v2)?;
self.shift_existing_cards(starting_from, step * cids.len() as u32, usn)?;
}
let cards = self.all_cards_for_ids(cids, true)?;
let sorter = NewCardSorter::new(&cards, starting_from, step, order);
let mut count = 0;
for mut card in cards {
let original = card.clone();
if card.set_new_position(sorter.position(&card), v2) {
if card.set_new_position(sorter.position(&card)) {
count += 1;
self.update_card_inner(&mut card, original, usn)?;
}
@ -287,10 +289,10 @@ impl Collection {
self.sort_cards_inner(&cids, 1, 1, order.into(), false, usn)
}
fn shift_existing_cards(&mut self, start: u32, by: u32, usn: Usn, v2: bool) -> Result<()> {
fn shift_existing_cards(&mut self, start: u32, by: u32, usn: Usn) -> Result<()> {
for mut card in self.storage.all_cards_at_or_above_position(start)? {
let original = card.clone();
card.set_new_position(card.due as u32 + by, v2);
card.set_new_position(card.due as u32 + by);
self.update_card_inner(&mut card, original, usn)?;
}
Ok(())

View File

@ -94,13 +94,6 @@ pub fn local_minutes_west_for_stamp(stamp: TimestampSecs) -> Result<i32> {
Ok(stamp.local_datetime()?.offset().utc_minus_local() / 60)
}
// Legacy code
// ----------------------------------
pub(crate) fn v1_rollover_from_creation_stamp(crt: TimestampSecs) -> Result<u8> {
crt.local_datetime().map(|dt| dt.hour() as u8)
}
pub(crate) fn v1_creation_date() -> i64 {
let now = TimestampSecs::now();
v1_creation_date_inner(now, local_minutes_west_for_stamp(now).unwrap())
@ -119,19 +112,6 @@ fn v1_creation_date_inner(now: TimestampSecs, mins_west: i32) -> i64 {
}
}
pub(crate) fn v1_creation_date_adjusted_to_hour(crt: TimestampSecs, hour: u8) -> Result<i64> {
let offset = fixed_offset_from_minutes(local_minutes_west_for_stamp(crt)?);
v1_creation_date_adjusted_to_hour_inner(crt, hour, offset)
}
fn v1_creation_date_adjusted_to_hour_inner(
crt: TimestampSecs,
hour: u8,
offset: FixedOffset,
) -> Result<i64> {
Ok(rollover_datetime(crt.datetime(offset)?, hour).timestamp())
}
fn sched_timing_today_v1(crt: TimestampSecs, now: TimestampSecs) -> SchedTimingToday {
let days_elapsed = (now.0 - crt.0) / 86_400;
let next_day_at = TimestampSecs(crt.0 + (days_elapsed + 1) * 86_400);
@ -494,19 +474,5 @@ mod test {
.unwrap()
.timestamp()
);
let crt = TimestampSecs(v1_creation_date_inner(now, AEST_MINS_WEST));
assert_eq!(
Ok(crt.0),
v1_creation_date_adjusted_to_hour_inner(crt, 4, offset)
);
assert_eq!(
Ok(crt.0 + 3600),
v1_creation_date_adjusted_to_hour_inner(crt, 5, offset)
);
assert_eq!(
Ok(crt.0 - 3600 * 4),
v1_creation_date_adjusted_to_hour_inner(crt, 0, offset)
);
}
}

View File

@ -581,8 +581,8 @@ impl super::SqliteStorage {
}
pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result<CongratsInfo> {
// FIXME: when v1/v2 are dropped, this line will become obsolete, as it's run
// on queue build by v3
// NOTE: this line is obsolete in v3 as it's run on queue build, but kept to
// prevent errors for v1/v2 users before they upgrade
self.update_active_decks(current)?;
self.db
.prepare(include_str!("congrats.sql"))?

View File

@ -14,28 +14,14 @@ SELECT did,
-- intraday learning
sum(
(
CASE
:sched_ver
WHEN 2 THEN (
-- v2 scheduler
(
queue = :learn_queue
AND due < :learn_cutoff
)
OR (
queue = :preview_queue
AND due <= :learn_cutoff
)
)
ELSE (
-- v1 scheduler
CASE
WHEN queue = :learn_queue
AND due < :learn_cutoff THEN left / 1000
ELSE 0
END
)
END
(
queue = :learn_queue
AND due < :learn_cutoff
)
OR (
queue = :preview_queue
AND due <= :learn_cutoff
)
)
),
-- total

View File

@ -13,7 +13,6 @@ use unicase::UniCase;
use super::SqliteStorage;
use crate::card::CardQueue;
use crate::config::SchedulerVersion;
use crate::decks::immediate_parent_name;
use crate::decks::DeckCommon;
use crate::decks::DeckKindContainer;
@ -297,16 +296,13 @@ impl SqliteStorage {
pub(crate) fn due_counts(
&self,
sched: SchedulerVersion,
day_cutoff: u32,
learn_cutoff: u32,
) -> Result<HashMap<DeckId, DueCounts>> {
let sched_ver = sched as u8;
let params = named_params! {
":new_queue": CardQueue::New as u8,
":review_queue": CardQueue::Review as u8,
":day_cutoff": day_cutoff,
":sched_ver": sched_ver,
":learn_queue": CardQueue::Learn as u8,
":learn_cutoff": learn_cutoff,
":daylearn_queue": CardQueue::DayLearn as u8,

View File

@ -133,20 +133,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}}
/>
<DynamicallySlottable slotHost={Item} {api}>
{#if state.v3Scheduler}
<Item>
<SwitchRow bind:value={$fsrs} defaultValue={false}>
<SettingTitle
on:click={() =>
openHelpModal(Object.keys(settings).indexOf("fsrs"))}
>
FSRS
</SettingTitle>
</SwitchRow>
</Item>
<Item>
<SwitchRow bind:value={$fsrs} defaultValue={false}>
<SettingTitle
on:click={() =>
openHelpModal(Object.keys(settings).indexOf("fsrs"))}
>
FSRS
</SettingTitle>
</SwitchRow>
</Item>
<Warning warning={fsrsClientWarning} />
{/if}
<Warning warning={fsrsClientWarning} />
<Item>
<SpinBoxRow
@ -164,7 +162,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SpinBoxRow>
</Item>
{#if !$fsrs || !state.v3Scheduler}
{#if !$fsrs}
<Item>
<SpinBoxFloatRow
bind:value={$config.initialEase}
@ -257,17 +255,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/>
{/if}
{#if state.v3Scheduler}
<Item>
<CardStateCustomizer
title={settings.customScheduling.title}
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf("customScheduling"),
)}
bind:value={$cardStateCustomizer}
/>
</Item>
{/if}
<Item>
<CardStateCustomizer
title={settings.customScheduling.title}
on:click={() =>
openHelpModal(Object.keys(settings).indexOf("customScheduling"))}
bind:value={$cardStateCustomizer}
/>
</Item>
</DynamicallySlottable>
</TitledContainer>

View File

@ -23,9 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const config = state.currentConfig;
const defaults = state.defaults;
const priorityTooltip = state.v3Scheduler
? "\n\n" + tr.deckConfigBuryPriorityTooltip()
: "";
const priorityTooltip = "\n\n" + tr.deckConfigBuryPriorityTooltip();
const settings = {
buryNewSiblings: {
@ -91,24 +89,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SwitchRow>
</Item>
{#if state.v3Scheduler}
<Item>
<SwitchRow
bind:value={$config.buryInterdayLearning}
defaultValue={defaults.buryInterdayLearning}
<Item>
<SwitchRow
bind:value={$config.buryInterdayLearning}
defaultValue={defaults.buryInterdayLearning}
>
<SettingTitle
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf(
"buryInterdayLearningSiblings",
),
)}
>
<SettingTitle
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf(
"buryInterdayLearningSiblings",
),
)}
>
{settings.buryInterdayLearningSiblings.title}
</SettingTitle>
</SwitchRow>
</Item>
{/if}
{settings.buryInterdayLearningSiblings.title}
</SettingTitle>
</SwitchRow>
</Item>
</DynamicallySlottable>
</TitledContainer>

View File

@ -44,25 +44,16 @@
const config = state.currentConfig;
const limits = state.deckLimits;
const defaults = state.defaults;
const parentLimits = state.parentLimits;
const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
const v3Extra = state.v3Scheduler
? "\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription()
: "";
const reviewV3Extra = state.v3Scheduler
? "\n\n" + tr.deckConfigLimitInterdayBoundByReviews() + v3Extra
: "";
const v3Extra =
"\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription();
const reviewV3Extra = "\n\n" + tr.deckConfigLimitInterdayBoundByReviews() + v3Extra;
const newCardsIgnoreReviewLimitHelp =
tr.deckConfigAffectsEntireCollection() +
"\n\n" +
tr.deckConfigNewCardsIgnoreReviewLimitTooltip();
$: newCardsGreaterThanParent =
!state.v3Scheduler && newValue > $parentLimits.newCards
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
: "";
$: reviewsTooLow =
Math.min(9999, newValue * 10) > reviewsValue
? tr.deckConfigReviewsTooLow({
@ -79,26 +70,21 @@
$config.newPerDay,
null,
),
].concat(
state.v3Scheduler
? [
new ValueTab(
tr.deckConfigDeckOnly(),
$limits.new ?? null,
(value) => ($limits.new = value ?? undefined),
null,
null,
),
new ValueTab(
tr.deckConfigTodayOnly(),
$limits.newTodayActive ? $limits.newToday ?? null : null,
(value) => ($limits.newToday = value ?? undefined),
null,
$limits.newToday ?? null,
),
]
: [],
);
new ValueTab(
tr.deckConfigDeckOnly(),
$limits.new ?? null,
(value) => ($limits.new = value ?? undefined),
null,
null,
),
new ValueTab(
tr.deckConfigTodayOnly(),
$limits.newTodayActive ? $limits.newToday ?? null : null,
(value) => ($limits.newToday = value ?? undefined),
null,
$limits.newToday ?? null,
),
];
const reviewTabs: ValueTab[] = [
new ValueTab(
@ -108,26 +94,21 @@
$config.reviewsPerDay,
null,
),
].concat(
state.v3Scheduler
? [
new ValueTab(
tr.deckConfigDeckOnly(),
$limits.review ?? null,
(value) => ($limits.review = value ?? undefined),
null,
null,
),
new ValueTab(
tr.deckConfigTodayOnly(),
$limits.reviewTodayActive ? $limits.reviewToday ?? null : null,
(value) => ($limits.reviewToday = value ?? undefined),
null,
$limits.reviewToday ?? null,
),
]
: [],
);
new ValueTab(
tr.deckConfigDeckOnly(),
$limits.review ?? null,
(value) => ($limits.review = value ?? undefined),
null,
null,
),
new ValueTab(
tr.deckConfigTodayOnly(),
$limits.reviewTodayActive ? $limits.reviewToday ?? null : null,
(value) => ($limits.reviewToday = value ?? undefined),
null,
$limits.reviewToday ?? null,
),
];
let newValue = 0;
let reviewsValue = 0;
@ -184,9 +165,6 @@
</SpinBoxRow>
</Item>
<Item>
<Warning warning={newCardsGreaterThanParent} />
</Item>
<Item>
<SpinBoxRow bind:value={reviewsValue} defaultValue={defaults.reviewsPerDay}>
<TabbedValue slot="tabs" tabs={reviewTabs} bind:value={reviewsValue} />
@ -203,21 +181,17 @@
<Warning warning={reviewsTooLow} />
</Item>
{#if state.v3Scheduler}
<Item>
<SwitchRow bind:value={$newCardsIgnoreReviewLimit} defaultValue={false}>
<SettingTitle
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf(
"newCardsIgnoreReviewLimit",
),
)}
>
{settings.newCardsIgnoreReviewLimit.title}
</SettingTitle>
</SwitchRow>
</Item>
{/if}
<Item>
<SwitchRow bind:value={$newCardsIgnoreReviewLimit} defaultValue={false}>
<SettingTitle
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf("newCardsIgnoreReviewLimit"),
)}
>
{settings.newCardsIgnoreReviewLimit.title}
</SettingTitle>
</SwitchRow>
</Item>
</DynamicallySlottable>
</TitledContainer>

View File

@ -86,13 +86,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</Row>
</Item>
{#if state.v3Scheduler}
<Item>
<Row class="row-columns">
<DisplayOrder {state} api={displayOrder} />
</Row>
</Item>
{/if}
<Item>
<Row class="row-columns">
<DisplayOrder {state} api={displayOrder} />
</Row>
</Item>
<Item>
<Row class="row-columns">

View File

@ -51,7 +51,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
: "";
$: insertionOrderRandom =
state.v3Scheduler &&
$config.newCardInsertOrder == DeckConfig_Config_NewCardInsertOrder.RANDOM
? tr.deckConfigNewInsertionOrderRandomWithV3()
: "";

View File

@ -72,7 +72,6 @@ const exampleData = {
currentDeck: {
name: "Default::child",
configId: 1618570764780n,
parentConfigIds: [1n],
},
defaults: {
config: {
@ -220,27 +219,6 @@ test("duplicate name", () => {
expect(get(state.configList).find((e) => e.current)?.name).toMatch(/Default\d+$/);
});
test("parent counts", () => {
const state = startingState();
expect(get(state.parentLimits)).toStrictEqual({ newCards: 10, reviews: 200 });
// adjusting the current deck config won't alter parent
state.currentConfig.update((c) => {
c.newPerDay = 123;
return c;
});
expect(get(state.parentLimits)).toStrictEqual({ newCards: 10, reviews: 200 });
// but adjusting the default config will, since the parent deck uses it
state.setCurrentIndex(1);
state.currentConfig.update((c) => {
c.newPerDay = 123;
return c;
});
expect(get(state.parentLimits)).toStrictEqual({ newCards: 123, reviews: 200 });
});
test("saving", () => {
let state = startingState();
let out = state.dataForSaving(false);

View File

@ -23,11 +23,6 @@ export interface ConfigWithCount {
useCount: number;
}
export interface ParentLimits {
newCards: number;
reviews: number;
}
/** Info for showing the top selector */
export interface ConfigListEntry {
idx: number;
@ -40,13 +35,11 @@ export class DeckOptionsState {
readonly currentConfig: Writable<DeckConfig_Config>;
readonly currentAuxData: Writable<Record<string, unknown>>;
readonly configList: Readable<ConfigListEntry[]>;
readonly parentLimits: Readable<ParentLimits>;
readonly cardStateCustomizer: Writable<string>;
readonly currentDeck: DeckConfigsForUpdate_CurrentDeck;
readonly deckLimits: Writable<DeckConfigsForUpdate_CurrentDeck_Limits>;
readonly defaults: DeckConfig_Config;
readonly addonComponents: Writable<DynamicSvelteComponent[]>;
readonly v3Scheduler: boolean;
readonly newCardsIgnoreReviewLimit: Writable<boolean>;
readonly fsrs: Writable<boolean>;
readonly currentPresetName: Writable<string>;
@ -55,7 +48,6 @@ export class DeckOptionsState {
private configs: ConfigWithCount[];
private selectedIdx: number;
private configListSetter!: (val: ConfigListEntry[]) => void;
private parentLimitsSetter!: (val: ParentLimits) => void;
private modifiedConfigs: Set<DeckOptionsId> = new Set();
private removedConfigs: DeckOptionsId[] = [];
private schemaModified: boolean;
@ -77,7 +69,6 @@ export class DeckOptionsState {
this.configs.findIndex((c) => c.config.id === this.currentDeck.configId),
);
this.sortConfigs();
this.v3Scheduler = data.v3Scheduler;
this.cardStateCustomizer = writable(data.cardStateCustomizer);
this.deckLimits = writable(data.currentDeck?.limits ?? createLimits());
this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit);
@ -93,17 +84,12 @@ export class DeckOptionsState {
this.configListSetter = set;
return;
});
this.parentLimits = readable(this.getParentLimits(), (set) => {
this.parentLimitsSetter = set;
return;
});
this.schemaModified = data.schemaModified;
this.addonComponents = writable([]);
// create a temporary subscription to force our setters to be set immediately,
// so unit tests don't get stale results
get(this.configList);
get(this.parentLimits);
// update our state when the current config is changed
this.currentConfig.subscribe((val) => this.onCurrentConfigChanged(val));
@ -227,7 +213,6 @@ export class DeckOptionsState {
this.modifiedConfigs.add(configOuter.id);
}
}
this.parentLimitsSetter?.(this.getParentLimits());
}
private onCurrentAuxDataChanged(data: Record<string, unknown>): void {
@ -253,7 +238,6 @@ export class DeckOptionsState {
private updateCurrentConfig(): void {
this.currentConfig.set(this.getCurrentConfig());
this.currentAuxData.set(this.getCurrentAuxData());
this.parentLimitsSetter?.(this.getParentLimits());
}
private updateConfigList(): void {
@ -292,22 +276,6 @@ export class DeckOptionsState {
});
return list;
}
private getParentLimits(): ParentLimits {
const parentConfigs = this.configs.filter((c) => this.currentDeck.parentConfigIds.includes(c.config.id));
const newCards = parentConfigs.reduce(
(previous, current) => Math.min(previous, current.config.config?.newPerDay ?? 0),
2 ** 31,
);
const reviews = parentConfigs.reduce(
(previous, current) => Math.min(previous, current.config.config?.reviewsPerDay ?? 0),
2 ** 31,
);
return {
newCards,
reviews,
};
}
}
function bytesToObject(bytes: Uint8Array): Record<string, unknown> {