Merge branch 'master' into top_toolbar_links_hook

This commit is contained in:
Aristotelis 2020-02-15 23:37:11 +01:00 committed by GitHub
commit da6c88afec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 911 additions and 545 deletions

View file

@ -48,8 +48,8 @@ class Card:
self.id = timestampID(col.db, "cards")
self.did = 1
self.crt = intTime()
self.type = 0
self.queue = 0
self.type = CARD_TYPE_NEW
self.queue = QUEUE_TYPE_NEW
self.ivl = 0
self.factor = 0
self.reps = 0
@ -84,13 +84,21 @@ class Card:
self._render_output = None
self._note = None
def flush(self) -> None:
def _preFlush(self) -> None:
hooks.card_will_flush(self)
self.mod = intTime()
self.usn = self.col.usn()
# bug check
if self.queue == 2 and self.odue and not self.col.decks.isDyn(self.did):
if (
self.queue == QUEUE_TYPE_REV
and self.odue
and not self.col.decks.isDyn(self.did)
):
hooks.card_odue_was_invalid()
assert self.due < 4294967296
def flush(self) -> None:
self._preFlush()
self.col.db.execute(
"""
insert or replace into cards values
@ -117,12 +125,8 @@ insert or replace into cards values
self.col.log(self)
def flushSched(self) -> None:
self.mod = intTime()
self.usn = self.col.usn()
self._preFlush()
# bug checks
if self.queue == 2 and self.odue and not self.col.decks.isDyn(self.did):
hooks.card_odue_was_invalid()
assert self.due < 4294967296
self.col.db.execute(
"""update cards set
mod=?, usn=?, type=?, queue=?, due=?, ivl=?, factor=?, reps=?,

View file

@ -14,6 +14,22 @@ NEW_CARDS_FIRST = 2
NEW_CARDS_RANDOM = 0
NEW_CARDS_DUE = 1
# Queue types
QUEUE_TYPE_MANUALLY_BURIED = -3
QUEUE_TYPE_SIBLING_BURIED = -2
QUEUE_TYPE_SUSPENDED = -1
QUEUE_TYPE_NEW = 0
QUEUE_TYPE_LRN = 1
QUEUE_TYPE_REV = 2
QUEUE_TYPE_DAY_LEARN_RELEARN = 3
QUEUE_TYPE_PREVIEW = 4
# Card types
CARD_TYPE_NEW = 0
CARD_TYPE_LRN = 1
CARD_TYPE_REV = 2
CARD_TYPE_RELEARNING = 3
# removal types
REM_CARD = 0
REM_NOTE = 1
@ -27,6 +43,10 @@ COUNT_REMAINING = 1
MEDIA_ADD = 0
MEDIA_REM = 1
# Kind of decks
DECK_STD = 0
DECK_DYN = 1
# dynamic deck order
DYN_OLDEST = 0
DYN_RANDOM = 1
@ -55,6 +75,22 @@ SYNC_VER = 9
HELP_SITE = "http://ankisrs.net/docs/manual.html"
# Leech actions
LEECH_SUSPEND = 0
LEECH_TAGONLY = 1
# Buttons
BUTTON_ONE = 1
BUTTON_TWO = 2
BUTTON_THREE = 3
BUTTON_FOUR = 4
# Revlog types
REVLOG_LRN = 0
REVLOG_REV = 1
REVLOG_RELRN = 2
REVLOG_CRAM = 3
# Labels
##########################################################################

View file

@ -27,7 +27,7 @@ defaultDeck = {
"conf": 1,
"usn": 0,
"desc": "",
"dyn": 0, # anki uses int/bool interchangably here
"dyn": DECK_STD, # anki uses int/bool interchangably here
"collapsed": False,
# added in beta11
"extendNew": 10,
@ -40,7 +40,7 @@ defaultDynamicDeck = {
"lrnToday": [0, 0],
"timeToday": [0, 0],
"collapsed": False,
"dyn": 1,
"dyn": DECK_DYN,
"desc": "",
"usn": 0,
"delays": None,
@ -71,7 +71,7 @@ defaultConf = {
"minInt": 1,
"leechFails": 8,
# type 0=suspend, 1=tagonly
"leechAction": 0,
"leechAction": LEECH_SUSPEND,
},
"rev": {
"perDay": 200,

View file

@ -240,7 +240,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """
elif type == "cardDue":
sort = "c.type, c.due"
elif type == "cardEase":
sort = "c.type == 0, c.factor"
sort = f"c.type == {CARD_TYPE_NEW}, c.factor"
elif type == "cardLapses":
sort = "c.lapses"
elif type == "cardIvl":
@ -271,18 +271,18 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """
if val == "review":
n = 2
elif val == "new":
n = 0
n = CARD_TYPE_NEW
else:
return "queue in (1, 3)"
return f"queue in ({QUEUE_TYPE_LRN}, {QUEUE_TYPE_DAY_LEARN_RELEARN})"
return "type = %d" % n
elif val == "suspended":
return "c.queue = -1"
elif val == "buried":
return "c.queue in (-2, -3)"
return f"c.queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})"
elif val == "due":
return """
(c.queue in (2,3) and c.due <= %d) or
(c.queue = 1 and c.due <= %d)""" % (
return f"""
(c.queue in ({QUEUE_TYPE_REV},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and c.due <= %d) or
(c.queue = {QUEUE_TYPE_LRN} and c.due <= %d)""" % (
self.col.sched.today,
self.col.sched.dayCutoff,
)
@ -349,7 +349,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """
if prop == "due":
val += self.col.sched.today
# only valid for review/daily learning
q.append("(c.queue in (2,3))")
q.append(f"(c.queue in ({QUEUE_TYPE_REV},{QUEUE_TYPE_DAY_LEARN_RELEARN}))")
elif prop == "ease":
prop = "factor"
val = int(val * 1000)

View file

@ -18,6 +18,7 @@ import decorator
import anki
from anki.cards import Card
from anki.notes import Note
# New hook/filter handling
##############################################################################
@ -133,6 +134,32 @@ class _CardOdueWasInvalidHook:
card_odue_was_invalid = _CardOdueWasInvalidHook()
class _CardWillFlushHook:
"""Allow to change a card before it is added/updated in the database."""
_hooks: List[Callable[[Card], None]] = []
def append(self, cb: Callable[[Card], None]) -> None:
"""(card: Card)"""
self._hooks.append(cb)
def remove(self, cb: Callable[[Card], None]) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(self, card: Card) -> None:
for hook in self._hooks:
try:
hook(card)
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
card_will_flush = _CardWillFlushHook()
class _DeckAddedHook:
_hooks: List[Callable[[Dict[str, Any]], None]] = []
@ -277,6 +304,32 @@ class _NoteTypeAddedHook:
note_type_added = _NoteTypeAddedHook()
class _NoteWillFlushHook:
"""Allow to change a note before it is added/updated in the database."""
_hooks: List[Callable[[Note], None]] = []
def append(self, cb: Callable[[Note], None]) -> None:
"""(note: Note)"""
self._hooks.append(cb)
def remove(self, cb: Callable[[Note], None]) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(self, note: Note) -> None:
for hook in self._hooks:
try:
hook(note)
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
note_will_flush = _NoteWillFlushHook()
class _NotesWillBeDeletedHook:
_hooks: List[Callable[["anki.storage._Collection", List[int]], None]] = []

View file

@ -6,6 +6,7 @@ import unicodedata
from typing import Any, Dict, List, Optional, Tuple
from anki.collection import _Collection
from anki.consts import *
from anki.importing.base import Importer
from anki.lang import _
from anki.storage import Collection
@ -343,7 +344,10 @@ class Anki2Importer(Importer):
card[4] = intTime()
card[5] = usn
# review cards have a due date relative to collection
if card[7] in (2, 3) or card[6] == 2:
if (
card[7] in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN)
or card[6] == CARD_TYPE_REV
):
card[8] -= aheadBy
# odue needs updating too
if card[14]:
@ -356,13 +360,13 @@ class Anki2Importer(Importer):
card[8] = card[14]
card[14] = 0
# queue
if card[6] == 1: # type
card[7] = 0
if card[6] == CARD_TYPE_LRN: # type
card[7] = QUEUE_TYPE_NEW
else:
card[7] = card[6]
# type
if card[6] == 1:
card[6] = 0
if card[6] == CARD_TYPE_LRN:
card[6] = CARD_TYPE_NEW
cards.append(card)
# we need to import revlog, rewriting card ids and bumping usn
for rev in self.src.db.execute("select * from revlog where cid = ?", scid):

View file

@ -52,9 +52,12 @@ class ForeignCard:
# If the first field of the model is not in the map, the map is invalid.
# The import mode is one of:
# 0: update if first field matches existing note
# 1: ignore if first field matches existing note
# 2: import even if first field matches existing note
# UPDATE_MODE: update if first field matches existing note
# IGNORE_MODE: ignore if first field matches existing note
# ADD_MODE: import even if first field matches existing note
UPDATE_MODE = 0
IGNORE_MODE = 1
ADD_MODE = 2
class NoteImporter(Importer):
@ -62,7 +65,7 @@ class NoteImporter(Importer):
needMapper = True
needDelimiter = False
allowHTML = False
importMode = 0
importMode = UPDATE_MODE
mapping: Optional[List[str]]
tagModified: Optional[str]
@ -153,7 +156,7 @@ class NoteImporter(Importer):
self.log.append(_("Empty first field: %s") % " ".join(n.fields))
continue
# earlier in import?
if fld0 in firsts and self.importMode != 2:
if fld0 in firsts and self.importMode != ADD_MODE:
# duplicates in source file; log and ignore
self.log.append(_("Appeared twice in file: %s") % fld0)
continue
@ -168,16 +171,16 @@ class NoteImporter(Importer):
if fld0 == sflds[0]:
# duplicate
found = True
if self.importMode == 0:
if self.importMode == UPDATE_MODE:
data = self.updateData(n, id, sflds)
if data:
updates.append(data)
updateLog.append(updateLogTxt % fld0)
dupeCount += 1
found = True
elif self.importMode == 1:
elif self.importMode == IGNORE_MODE:
dupeCount += 1
elif self.importMode == 2:
elif self.importMode == ADD_MODE:
# allow duplicates in this case
if fld0 not in dupes:
# only show message once, no matter how many
@ -214,9 +217,9 @@ class NoteImporter(Importer):
ngettext("%d note updated", "%d notes updated", self.updateCount)
% self.updateCount
)
if self.importMode == 0:
if self.importMode == UPDATE_MODE:
unchanged = dupeCount - self.updateCount
elif self.importMode == 1:
elif self.importMode == IGNORE_MODE:
unchanged = dupeCount
else:
unchanged = 0

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
import anki # pylint: disable=unused-import
from anki import hooks
from anki.models import Field, NoteType
from anki.utils import (
fieldChecksum,
@ -202,6 +203,7 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
##################################################
def _preFlush(self) -> None:
hooks.note_will_flush(self)
# have we been added yet?
self.newlyAdded = not self.col.db.scalar(
"select 1 from cards where nid = ?", self.id

View file

@ -67,28 +67,28 @@ class Scheduler:
self._burySiblings(card)
card.reps += 1
# former is for logging new cards, latter also covers filt. decks
card.wasNew = card.type == 0
wasNewQ = card.queue == 0
card.wasNew = card.type == CARD_TYPE_NEW
wasNewQ = card.queue == QUEUE_TYPE_NEW
if wasNewQ:
# came from the new queue, move to learning
card.queue = 1
card.queue = QUEUE_TYPE_LRN
# if it was a new card, it's now a learning card
if card.type == 0:
card.type = 1
if card.type == CARD_TYPE_NEW:
card.type = CARD_TYPE_LRN
# init reps to graduation
card.left = self._startingLeft(card)
# dynamic?
if card.odid and card.type == 2:
if card.odid and card.type == CARD_TYPE_REV:
if self._resched(card):
# reviews get their ivl boosted on first sight
card.ivl = self._dynIvlBoost(card)
card.odue = self.today + card.ivl
self._updateStats(card, "new")
if card.queue in (1, 3):
if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
self._answerLrnCard(card, ease)
if not wasNewQ:
self._updateStats(card, "lrn")
elif card.queue == 2:
elif card.queue == QUEUE_TYPE_REV:
self._answerRevCard(card, ease)
self._updateStats(card, "rev")
else:
@ -112,9 +112,9 @@ class Scheduler:
"Return counts over next DAYS. Includes today."
daysd = dict(
self.col.db.all(
"""
f"""
select due, count() from cards
where did in %s and queue = 2
where did in %s and queue = {QUEUE_TYPE_REV}
and due between ? and ?
group by due
order by due"""
@ -132,20 +132,20 @@ order by due"""
return ret
def countIdx(self, card):
if card.queue == 3:
if card.queue == QUEUE_TYPE_DAY_LEARN_RELEARN:
return 1
return card.queue
def answerButtons(self, card):
if card.odue:
# normal review in dyn deck?
if card.odid and card.queue == 2:
if card.odid and card.queue == QUEUE_TYPE_REV:
return 4
conf = self._lrnConf(card)
if card.type in (0, 1) or len(conf["delays"]) > 1:
if card.type in (CARD_TYPE_NEW, CARD_TYPE_LRN) or len(conf["delays"]) > 1:
return 3
return 2
elif card.queue == 2:
elif card.queue == QUEUE_TYPE_REV:
return 4
else:
return 3
@ -153,18 +153,25 @@ order by due"""
def unburyCards(self):
"Unbury cards."
self.col.conf["lastUnburied"] = self.today
self.col.log(self.col.db.list("select id from cards where queue = -2"))
self.col.db.execute("update cards set queue=type where queue = -2")
self.col.log(
self.col.db.list(
f"select id from cards where queue = {QUEUE_TYPE_SIBLING_BURIED}"
)
)
self.col.db.execute(
f"update cards set queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED}"
)
def unburyCardsForDeck(self):
sids = ids2str(self.col.decks.active())
self.col.log(
self.col.db.list(
"select id from cards where queue = -2 and did in %s" % sids
f"select id from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s"
% sids
)
)
self.col.db.execute(
"update cards set mod=?,usn=?,queue=type where queue = -2 and did in %s"
f"update cards set mod=?,usn=?,queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s"
% sids,
intTime(),
self.col.usn(),
@ -348,9 +355,9 @@ order by due"""
def _resetNewCount(self):
cntFn = lambda did, lim: self.col.db.scalar(
"""
f"""
select count() from (select 1 from cards where
did = ? and queue = 0 limit ?)""",
did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
did,
lim,
)
@ -373,8 +380,8 @@ did = ? and queue = 0 limit ?)""",
if lim:
# fill the queue with the current did
self._newQueue = self.col.db.list(
"""
select id from cards where did = ? and queue = 0 order by due,ord limit ?""",
f"""
select id from cards where did = ? and queue = {QUEUE_TYPE_NEW} order by due,ord limit ?""",
did,
lim,
)
@ -436,9 +443,9 @@ did = ? and queue = 0 limit ?)""",
return 0
lim = min(lim, self.reportLimit)
return self.col.db.scalar(
"""
f"""
select count() from
(select 1 from cards where did = ? and queue = 0 limit ?)""",
(select 1 from cards where did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
did,
lim,
)
@ -452,9 +459,9 @@ select count() from
def totalNewForCurrentDeck(self):
return self.col.db.scalar(
"""
f"""
select count() from cards where id in (
select id from cards where did in %s and queue = 0 limit ?)"""
select id from cards where did in %s and queue = {QUEUE_TYPE_NEW} limit ?)"""
% ids2str(self.col.decks.active()),
self.reportLimit,
)
@ -466,9 +473,9 @@ select id from cards where did in %s and queue = 0 limit ?)"""
# sub-day
self.lrnCount = (
self.col.db.scalar(
"""
f"""
select sum(left/1000) from (select left from cards where
did in %s and queue = 1 and due < ? limit %d)"""
did in %s and queue = {QUEUE_TYPE_LRN} and due < ? limit %d)"""
% (self._deckLimit(), self.reportLimit),
self.dayCutoff,
)
@ -476,8 +483,8 @@ did in %s and queue = 1 and due < ? limit %d)"""
)
# day
self.lrnCount += self.col.db.scalar(
"""
select count() from cards where did in %s and queue = 3
f"""
select count() from cards where did in %s and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN}
and due <= ? limit %d"""
% (self._deckLimit(), self.reportLimit),
self.today,
@ -496,9 +503,9 @@ and due <= ? limit %d"""
if self._lrnQueue:
return True
self._lrnQueue = self.col.db.all(
"""
f"""
select due, id from cards where
did in %s and queue = 1 and due < :lim
did in %s and queue = {QUEUE_TYPE_LRN} and due < :lim
limit %d"""
% (self._deckLimit(), self.reportLimit),
lim=self.dayCutoff,
@ -528,9 +535,9 @@ limit %d"""
did = self._lrnDids[0]
# fill the queue with the current did
self._lrnDayQueue = self.col.db.list(
"""
f"""
select id from cards where
did = ? and queue = 3 and due <= ? limit ?""",
did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
did,
self.today,
self.queueLimit,
@ -556,25 +563,25 @@ did = ? and queue = 3 and due <= ? limit ?""",
# ease 1=no, 2=yes, 3=remove
conf = self._lrnConf(card)
if card.odid and not card.wasNew:
type = 3
elif card.type == 2:
type = 2
type = REVLOG_CRAM
elif card.type == CARD_TYPE_REV:
type = REVLOG_RELRN
else:
type = 0
type = REVLOG_LRN
leaving = False
# lrnCount was decremented once when card was fetched
lastLeft = card.left
# immediate graduate?
if ease == 3:
if ease == BUTTON_THREE:
self._rescheduleAsRev(card, conf, True)
leaving = True
# graduation time?
elif ease == 2 and (card.left % 1000) - 1 <= 0:
elif ease == BUTTON_TWO and (card.left % 1000) - 1 <= 0:
self._rescheduleAsRev(card, conf, False)
leaving = True
else:
# one step towards graduation
if ease == 2:
if ease == BUTTON_TWO:
# decrement real left count and recalculate left today
left = (card.left % 1000) - 1
card.left = self._leftToday(conf["delays"], left) * 1000 + left
@ -601,7 +608,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
# if the queue is not empty and there's nothing else to do, make
# sure we don't put it at the head of the queue and end up showing
# it twice in a row
card.queue = 1
card.queue = QUEUE_TYPE_LRN
if self._lrnQueue and not self.revCount and not self.newCount:
smallestDue = self._lrnQueue[0][0]
card.due = max(card.due, smallestDue + 1)
@ -611,7 +618,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
# day learn queue
ahead = ((card.due - self.dayCutoff) // 86400) + 1
card.due = self.today + ahead
card.queue = 3
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
self._logLrn(card, ease, conf, leaving, type, lastLeft)
def _delayForGrade(self, conf, left):
@ -627,13 +634,13 @@ did = ? and queue = 3 and due <= ? limit ?""",
return delay * 60
def _lrnConf(self, card):
if card.type == 2:
if card.type == CARD_TYPE_REV:
return self._lapseConf(card)
else:
return self._newConf(card)
def _rescheduleAsRev(self, card, conf, early):
lapse = card.type == 2
lapse = card.type == CARD_TYPE_REV
if lapse:
if self._resched(card):
card.due = max(self.today + 1, card.odue)
@ -642,8 +649,8 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.odue = 0
else:
self._rescheduleNew(card, conf, early)
card.queue = 2
card.type = 2
card.queue = QUEUE_TYPE_REV
card.type = CARD_TYPE_REV
# if we were dynamic, graduating means moving back to the old deck
resched = self._resched(card)
if card.odid:
@ -652,11 +659,11 @@ did = ? and queue = 3 and due <= ? limit ?""",
card.odid = 0
# if rescheduling is off, it needs to be set back to a new card
if not resched and not lapse:
card.queue = card.type = 0
card.queue = card.type = CARD_TYPE_NEW
card.due = self.col.nextID("pos")
def _startingLeft(self, card):
if card.type == 2:
if card.type == CARD_TYPE_REV:
conf = self._lapseConf(card)
else:
conf = self._lrnConf(card)
@ -678,7 +685,7 @@ did = ? and queue = 3 and due <= ? limit ?""",
return ok + 1
def _graduatingIvl(self, card, conf, early, adj=True):
if card.type == 2:
if card.type == CARD_TYPE_REV:
# lapsed card being relearnt
if card.odid:
if conf["resched"]:
@ -736,25 +743,28 @@ did = ? and queue = 3 and due <= ? limit ?""",
extra = " and did in " + ids2str(self.col.decks.allIds())
# review cards in relearning
self.col.db.execute(
"""
f"""
update cards set
due = odue, queue = 2, mod = %d, usn = %d, odue = 0
where queue in (1,3) and type = 2
due = odue, queue = {QUEUE_TYPE_REV}, mod = %d, usn = %d, odue = 0
where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CARD_TYPE_REV}
%s
"""
% (intTime(), self.col.usn(), extra)
)
# new cards in learning
self.forgetCards(
self.col.db.list("select id from cards where queue in (1,3) %s" % extra)
self.col.db.list(
f"select id from cards where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) %s"
% extra
)
)
def _lrnForDeck(self, did):
cnt = (
self.col.db.scalar(
"""
f"""
select sum(left/1000) from
(select left from cards where did = ? and queue = 1 and due < ? limit ?)""",
(select left from cards where did = ? and queue = {QUEUE_TYPE_LRN} and due < ? limit ?)""",
did,
intTime() + self.col.conf["collapseTime"],
self.reportLimit,
@ -762,9 +772,9 @@ select sum(left/1000) from
or 0
)
return cnt + self.col.db.scalar(
"""
f"""
select count() from
(select 1 from cards where did = ? and queue = 3
(select 1 from cards where did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN}
and due <= ? limit ?)""",
did,
self.today,
@ -786,9 +796,9 @@ and due <= ? limit ?)""",
def _revForDeck(self, did, lim):
lim = min(lim, self.reportLimit)
return self.col.db.scalar(
"""
f"""
select count() from
(select 1 from cards where did = ? and queue = 2
(select 1 from cards where did = ? and queue = {QUEUE_TYPE_REV}
and due <= ? limit ?)""",
did,
self.today,
@ -798,9 +808,9 @@ and due <= ? limit ?)""",
def _resetRevCount(self):
def cntFn(did, lim):
return self.col.db.scalar(
"""
f"""
select count() from (select id from cards where
did = ? and queue = 2 and due <= ? limit %d)"""
did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit %d)"""
% lim,
did,
self.today,
@ -824,9 +834,9 @@ did = ? and queue = 2 and due <= ? limit %d)"""
if lim:
# fill the queue with the current did
self._revQueue = self.col.db.list(
"""
f"""
select id from cards where
did = ? and queue = 2 and due <= ? limit ?""",
did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
did,
self.today,
lim,
@ -861,9 +871,9 @@ did = ? and queue = 2 and due <= ? limit ?""",
def totalRevForCurrentDeck(self):
return self.col.db.scalar(
"""
f"""
select count() from cards where id in (
select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit ?)"""
% ids2str(self.col.decks.active()),
self.today,
self.reportLimit,
@ -874,7 +884,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
def _answerRevCard(self, card, ease):
delay = 0
if ease == 1:
if ease == BUTTON_ONE:
delay = self._rescheduleLapse(card)
else:
self._rescheduleRev(card, ease)
@ -893,7 +903,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
card.odue = card.due
# if suspended as a leech, nothing to do
delay = 0
if self._checkLeech(card, conf) and card.queue == -1:
if self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED:
return delay
# if no relearning steps, nothing to do
if not conf["delays"]:
@ -907,13 +917,13 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# queue 1
if card.due < self.dayCutoff:
self.lrnCount += card.left // 1000
card.queue = 1
card.queue = QUEUE_TYPE_LRN
heappush(self._lrnQueue, (card.due, card.id))
else:
# day learn queue
ahead = ((card.due - self.dayCutoff) // 86400) + 1
card.due = self.today + ahead
card.queue = 3
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
return delay
def _nextLapseIvl(self, card, conf):
@ -946,7 +956,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
card.lastIvl,
card.factor,
card.timeTaken(),
1,
REVLOG_REV,
)
try:
@ -969,11 +979,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
ivl4 = self._constrainedIvl(
(card.ivl + delay) * fct * conf["ease4"], conf, ivl3
)
if ease == 2:
if ease == BUTTON_TWO:
interval = ivl2
elif ease == 3:
elif ease == BUTTON_THREE:
interval = ivl3
elif ease == 4:
elif ease == BUTTON_FOUR:
interval = ivl4
# interval capped?
return min(interval, conf["maxIvl"])
@ -1058,9 +1068,9 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self.col.log(self.col.db.list("select id from cards where %s" % lim))
# move out of cram queue
self.col.db.execute(
"""
update cards set did = odid, queue = (case when type = 1 then 0
else type end), type = (case when type = 1 then 0 else type end),
f"""
update cards set did = odid, queue = (case when type = {CARD_TYPE_LRN} then {QUEUE_TYPE_NEW}
else type end), type = (case when type = {CARD_TYPE_LRN} then {CARD_TYPE_NEW} else type end),
due = odue, odue = 0, odid = 0, usn = ? where %s"""
% lim,
self.col.usn(),
@ -1088,7 +1098,7 @@ due = odue, odue = 0, odid = 0, usn = ? where %s"""
t = "c.due"
elif o == DYN_DUEPRIORITY:
t = (
"(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)"
f"(case when queue={QUEUE_TYPE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)"
% (self.today, self.today)
)
else:
@ -1106,9 +1116,9 @@ due = odue, odue = 0, odid = 0, usn = ? where %s"""
data.append((did, -100000 + c, u, id))
# due reviews stay in the review queue. careful: can't use
# "odid or did", as sqlite converts to boolean
queue = """
(case when type=2 and (case when odue then odue <= %d else due <= %d end)
then 2 else 0 end)"""
queue = f"""
(case when type={CARD_TYPE_REV} and (case when odue then odue <= %d else due <= %d end)
then {QUEUE_TYPE_REV} else {QUEUE_TYPE_NEW} end)"""
queue %= (self.today, self.today)
self.col.db.executemany(
"""
@ -1121,7 +1131,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
)
def _dynIvlBoost(self, card):
assert card.odid and card.type == 2
assert card.odid and card.type == CARD_TYPE_REV
assert card.factor
elapsed = card.ivl - (card.odue - self.today)
factor = ((card.factor / 1000) + 1.2) / 2
@ -1145,14 +1155,14 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
f.flush()
# handle
a = conf["leechAction"]
if a == 0:
if a == LEECH_SUSPEND:
# if it has an old due, remove it from cram/relearning
if card.odue:
card.due = card.odue
if card.odid:
card.did = card.odid
card.odue = card.odid = 0
card.queue = -1
card.queue = QUEUE_TYPE_SUSPENDED
# notify UI
hooks.card_did_leech(card)
return True
@ -1311,7 +1321,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
"True if there are any rev cards due."
return self.col.db.scalar(
(
"select 1 from cards where did in %s and queue = 2 "
f"select 1 from cards where did in %s and queue = {QUEUE_TYPE_REV} "
"and due <= ? limit 1"
)
% self._deckLimit(),
@ -1321,14 +1331,18 @@ To study outside of the normal schedule, click the Custom Study button below."""
def newDue(self):
"True if there are any new cards due."
return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 0 " "limit 1")
(
f"select 1 from cards where did in %s and queue = {QUEUE_TYPE_NEW} "
"limit 1"
)
% self._deckLimit()
)
def haveBuried(self):
sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar(
"select 1 from cards where queue = -2 and did in %s limit 1" % sdids
f"select 1 from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s limit 1"
% sdids
)
return not not cnt
@ -1347,9 +1361,9 @@ To study outside of the normal schedule, click the Custom Study button below."""
def nextIvl(self, card, ease):
"Return the next interval for CARD, in seconds."
if card.queue in (0, 1, 3):
if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
return self._nextLrnIvl(card, ease)
elif ease == 1:
elif ease == BUTTON_ONE:
# lapsed
conf = self._lapseConf(card)
if conf["delays"]:
@ -1364,10 +1378,10 @@ To study outside of the normal schedule, click the Custom Study button below."""
if card.queue == 0:
card.left = self._startingLeft(card)
conf = self._lrnConf(card)
if ease == 1:
if ease == BUTTON_ONE:
# fail
return self._delayForGrade(conf, len(conf["delays"]))
elif ease == 3:
elif ease == BUTTON_THREE:
# early removal
if not self._resched(card):
return 0
@ -1391,7 +1405,8 @@ To study outside of the normal schedule, click the Custom Study button below."""
self.remFromDyn(ids)
self.removeLrn(ids)
self.col.db.execute(
"update cards set queue=-1,mod=?,usn=? where id in " + ids2str(ids),
f"update cards set queue={QUEUE_TYPE_SUSPENDED},mod=?,usn=? where id in "
+ ids2str(ids),
intTime(),
self.col.usn(),
)
@ -1401,7 +1416,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
self.col.log(ids)
self.col.db.execute(
"update cards set queue=type,mod=?,usn=? "
"where queue = -1 and id in " + ids2str(ids),
f"where queue = {QUEUE_TYPE_SUSPENDED} and id in " + ids2str(ids),
intTime(),
self.col.usn(),
)
@ -1411,8 +1426,8 @@ To study outside of the normal schedule, click the Custom Study button below."""
self.remFromDyn(cids)
self.removeLrn(cids)
self.col.db.execute(
"""
update cards set queue=-2,mod=?,usn=? where id in """
f"""
update cards set queue={QUEUE_TYPE_SIBLING_BURIED},mod=?,usn=? where id in """
+ ids2str(cids),
intTime(),
self.col.usn(),
@ -1436,14 +1451,14 @@ update cards set queue=-2,mod=?,usn=? where id in """
buryRev = rconf.get("bury", True)
# loop through and remove from queues
for cid, queue in self.col.db.execute(
"""
f"""
select id, queue from cards where nid=? and id!=?
and (queue=0 or (queue=2 and due<=?))""",
and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
card.nid,
card.id,
self.today,
):
if queue == 2:
if queue == QUEUE_TYPE_REV:
if buryRev:
toBury.append(cid)
# if bury disabled, we still discard to give same-day spacing
@ -1462,7 +1477,8 @@ and (queue=0 or (queue=2 and due<=?))""",
# then bury
if toBury:
self.col.db.execute(
"update cards set queue=-2,mod=?,usn=? where id in " + ids2str(toBury),
f"update cards set queue={QUEUE_TYPE_SIBLING_BURIED},mod=?,usn=? where id in "
+ ids2str(toBury),
intTime(),
self.col.usn(),
)
@ -1475,11 +1491,14 @@ and (queue=0 or (queue=2 and due<=?))""",
"Put cards at the end of the new queue."
self.remFromDyn(ids)
self.col.db.execute(
"update cards set type=0,queue=0,ivl=0,due=0,odue=0,factor=?"
f"update cards set type={CARD_TYPE_NEW},queue={QUEUE_TYPE_NEW},ivl=0,due=0,odue=0,factor=?"
" where id in " + ids2str(ids),
STARTING_FACTOR,
)
pmax = self.col.db.scalar("select max(due) from cards where type=0") or 0
pmax = (
self.col.db.scalar(f"select max(due) from cards where type={CARD_TYPE_NEW}")
or 0
)
# takes care of mod + usn
self.sortCards(ids, start=pmax + 1)
self.col.log(ids)
@ -1503,8 +1522,8 @@ and (queue=0 or (queue=2 and due<=?))""",
)
self.remFromDyn(ids)
self.col.db.executemany(
"""
update cards set type=2,queue=2,ivl=:ivl,due=:due,odue=0,
f"""
update cards set type={CARD_TYPE_REV},queue={QUEUE_TYPE_REV},ivl=:ivl,due=:due,odue=0,
usn=:usn,mod=:mod,factor=:fact where id=:id""",
d,
)
@ -1515,11 +1534,12 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
sids = ids2str(ids)
# we want to avoid resetting due number of existing new cards on export
nonNew = self.col.db.list(
"select id from cards where id in %s and (queue != 0 or type != 0)" % sids
f"select id from cards where id in %s and (queue != {QUEUE_TYPE_NEW} or type != {CARD_TYPE_NEW})"
% sids
)
# reset all cards
self.col.db.execute(
"update cards set reps=0,lapses=0,odid=0,odue=0,queue=0"
f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_TYPE_NEW}"
" where id in %s" % sids
)
# and forget any non-new cards, changing their due numbers
@ -1553,16 +1573,16 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
# shift?
if shift:
low = self.col.db.scalar(
"select min(due) from cards where due >= ? and type = 0 "
f"select min(due) from cards where due >= ? and type = {CARD_TYPE_NEW} "
"and id not in %s" % scids,
start,
)
if low is not None:
shiftby = high - low + 1
self.col.db.execute(
"""
f"""
update cards set mod=?, usn=?, due=due+? where id not in %s
and due >= ? and queue = 0"""
and due >= ? and queue = {QUEUE_TYPE_NEW}"""
% scids,
now,
self.col.usn(),
@ -1572,7 +1592,7 @@ and due >= ? and queue = 0"""
# reorder cards
d = []
for id, nid in self.col.db.execute(
"select id, nid from cards where type = 0 and id in " + scids
f"select id, nid from cards where type = {CARD_TYPE_NEW} and id in " + scids
):
d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id))
self.col.db.executemany(

View file

@ -22,13 +22,9 @@ from anki.rsbackend import SchedTimingToday
from anki.utils import fmtTimeSpan, ids2str, intTime
# card types: 0=new, 1=lrn, 2=rev, 3=relrn
CARD_TYPE_RELEARNING = 3
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
# 4=preview, -1=suspended, -2=sibling buried, -3=manually buried
QUEUE_TYPE_PREVIEW = 4
QUEUE_TYPE_DAY_LEARN_RELEARN = 3
QUEUE_TYPE_SIBLING_BURIED = -2
QUEUE_TYPE_MANUALLY_BURIED = -3
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=early review
# positive revlog intervals are in days (rev), negative in seconds (lrn)
# odue/odid store original due/did when cards moved to filtered deck
@ -95,18 +91,18 @@ class Scheduler:
card.reps += 1
if card.queue == 0:
if card.queue == QUEUE_TYPE_NEW:
# came from the new queue, move to learning
card.queue = 1
card.type = 1
card.queue = QUEUE_TYPE_LRN
card.type = CARD_TYPE_LRN
# init reps to graduation
card.left = self._startingLeft(card)
# update daily limit
self._updateStats(card, "new")
if card.queue in (1, QUEUE_TYPE_DAY_LEARN_RELEARN):
if card.queue in (QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
self._answerLrnCard(card, ease)
elif card.queue == 2:
elif card.queue == QUEUE_TYPE_REV:
self._answerRevCard(card, ease)
# update daily limit
self._updateStats(card, "rev")
@ -121,12 +117,13 @@ class Scheduler:
def _answerCardPreview(self, card: Card, ease: int) -> None:
assert 1 <= ease <= 2
if ease == 1:
if ease == BUTTON_ONE:
# repeat after delay
card.queue = QUEUE_TYPE_PREVIEW
card.due = intTime() + self._previewDelay(card)
self.lrnCount += 1
else:
# BUTTON_TWO
# restore original card state and remove from filtered deck
self._restorePreviewCard(card)
self._removeFromFiltered(card)
@ -142,9 +139,9 @@ class Scheduler:
"Return counts over next DAYS. Includes today."
daysd = dict(
self.col.db.all(
"""
f"""
select due, count() from cards
where did in %s and queue = 2
where did in %s and queue = {QUEUE_TYPE_REV}
and due between ? and ?
group by due
order by due"""
@ -368,9 +365,9 @@ order by due"""
def _resetNewCount(self) -> None:
cntFn = lambda did, lim: self.col.db.scalar(
"""
f"""
select count() from (select 1 from cards where
did = ? and queue = 0 limit ?)""",
did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
did,
lim,
)
@ -393,8 +390,8 @@ did = ? and queue = 0 limit ?)""",
if lim:
# fill the queue with the current did
self._newQueue = self.col.db.list(
"""
select id from cards where did = ? and queue = 0 order by due,ord limit ?""",
f"""
select id from cards where did = ? and queue = {QUEUE_TYPE_NEW} order by due,ord limit ?""",
did,
lim,
)
@ -462,9 +459,9 @@ did = ? and queue = 0 limit ?)""",
return 0
lim = min(lim, self.reportLimit)
return self.col.db.scalar(
"""
f"""
select count() from
(select 1 from cards where did = ? and queue = 0 limit ?)""",
(select 1 from cards where did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
did,
lim,
)
@ -478,9 +475,9 @@ select count() from
def totalNewForCurrentDeck(self) -> Any:
return self.col.db.scalar(
"""
f"""
select count() from cards where id in (
select id from cards where did in %s and queue = 0 limit ?)"""
select id from cards where did in %s and queue = {QUEUE_TYPE_NEW} limit ?)"""
% self._deckLimit(),
self.reportLimit,
)
@ -504,8 +501,8 @@ select id from cards where did in %s and queue = 0 limit ?)"""
# sub-day
self.lrnCount = (
self.col.db.scalar(
"""
select count() from cards where did in %s and queue = 1
f"""
select count() from cards where did in %s and queue = {QUEUE_TYPE_LRN}
and due < ?"""
% (self._deckLimit()),
self._lrnCutoff,
@ -545,7 +542,7 @@ select count() from cards where did in %s and queue = {QUEUE_TYPE_PREVIEW}
self._lrnQueue = self.col.db.all(
f"""
select due, id from cards where
did in %s and queue in (1,{QUEUE_TYPE_PREVIEW}) and due < :lim
did in %s and queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_PREVIEW}) and due < :lim
limit %d"""
% (self._deckLimit(), self.reportLimit),
lim=cutoff,
@ -606,28 +603,28 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
def _answerLrnCard(self, card: Card, ease: int) -> None:
conf = self._lrnConf(card)
if card.type in (2, CARD_TYPE_RELEARNING):
type = 2
if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING):
type = REVLOG_RELRN
else:
type = 0
type = REVLOG_LRN
# lrnCount was decremented once when card was fetched
lastLeft = card.left
leaving = False
# immediate graduate?
if ease == 4:
if ease == BUTTON_FOUR:
self._rescheduleAsRev(card, conf, True)
leaving = True
# next step?
elif ease == 3:
elif ease == BUTTON_THREE:
# graduation time?
if (card.left % 1000) - 1 <= 0:
self._rescheduleAsRev(card, conf, False)
leaving = True
else:
self._moveToNextStep(card, conf)
elif ease == 2:
elif ease == BUTTON_TWO:
self._repeatStep(card, conf)
else:
# back to first step
@ -673,7 +670,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
maxExtra = min(300, int(delay * 0.25))
fuzz = random.randrange(0, maxExtra)
card.due = min(self.dayCutoff - 1, card.due + fuzz)
card.queue = 1
card.queue = QUEUE_TYPE_LRN
if card.due < (intTime() + self.col.conf["collapseTime"]):
self.lrnCount += 1
# if the queue is not empty and there's nothing else to do, make
@ -714,13 +711,13 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
return avg
def _lrnConf(self, card: Card) -> Any:
if card.type in (2, CARD_TYPE_RELEARNING):
if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING):
return self._lapseConf(card)
else:
return self._newConf(card)
def _rescheduleAsRev(self, card: Card, conf: Dict[str, Any], early: bool) -> None:
lapse = card.type in (2, CARD_TYPE_RELEARNING)
lapse = card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING)
if lapse:
self._rescheduleGraduatingLapse(card, early)
@ -735,8 +732,8 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
if early:
card.ivl += 1
card.due = self.today + card.ivl
card.queue = 2
card.type = 2
card.queue = QUEUE_TYPE_REV
card.type = CARD_TYPE_REV
def _startingLeft(self, card: Card) -> int:
if card.type == CARD_TYPE_RELEARNING:
@ -768,7 +765,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
def _graduatingIvl(
self, card: Card, conf: Dict[str, Any], early: bool, fuzz: bool = True
) -> Any:
if card.type in (2, CARD_TYPE_RELEARNING):
if card.type in (CARD_TYPE_REV, CARD_TYPE_RELEARNING):
bonus = early and 1 or 0
return card.ivl + bonus
if not early:
@ -786,7 +783,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today + card.ivl
card.factor = conf["initialFactor"]
card.type = card.queue = 2
card.type = card.queue = QUEUE_TYPE_REV
def _logLrn(
self,
@ -801,7 +798,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
if leaving:
ivl = card.ivl
else:
if ease == 2:
if ease == BUTTON_TWO:
ivl = -self._delayForRepeatingGrade(conf, card.left)
else:
ivl = -self._delayForGrade(conf, card.left)
@ -830,9 +827,9 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
def _lrnForDeck(self, did: int) -> Any:
cnt = (
self.col.db.scalar(
"""
f"""
select count() from
(select null from cards where did = ? and queue = 1 and due < ? limit ?)""",
(select null from cards where did = ? and queue = {QUEUE_TYPE_LRN} and due < ? limit ?)""",
did,
intTime() + self.col.conf["collapseTime"],
self.reportLimit,
@ -883,9 +880,9 @@ and due <= ? limit ?)""",
dids = [did] + self.col.decks.childDids(did, childMap)
lim = min(lim, self.reportLimit)
return self.col.db.scalar(
"""
f"""
select count() from
(select 1 from cards where did in %s and queue = 2
(select 1 from cards where did in %s and queue = {QUEUE_TYPE_REV}
and due <= ? limit ?)"""
% ids2str(dids),
self.today,
@ -895,9 +892,9 @@ and due <= ? limit ?)"""
def _resetRevCount(self) -> None:
lim = self._currentRevLimit()
self.revCount = self.col.db.scalar(
"""
f"""
select count() from (select id from cards where
did in %s and queue = 2 and due <= ? limit ?)"""
did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit ?)"""
% self._deckLimit(),
self.today,
lim,
@ -916,9 +913,9 @@ did in %s and queue = 2 and due <= ? limit ?)"""
lim = min(self.queueLimit, self._currentRevLimit())
if lim:
self._revQueue = self.col.db.list(
"""
f"""
select id from cards where
did in %s and queue = 2 and due <= ?
did in %s and queue = {QUEUE_TYPE_REV} and due <= ?
order by due, random()
limit ?"""
% self._deckLimit(),
@ -946,9 +943,9 @@ limit ?"""
def totalRevForCurrentDeck(self) -> int:
return self.col.db.scalar(
"""
f"""
select count() from cards where id in (
select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? limit ?)"""
% self._deckLimit(),
self.today,
self.reportLimit,
@ -960,9 +957,9 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
def _answerRevCard(self, card: Card, ease: int) -> None:
delay = 0
early = bool(card.odid and (card.odue > self.today))
type = early and 3 or 1
type = early and REVLOG_CRAM or REVLOG_REV
if ease == 1:
if ease == BUTTON_ONE:
delay = self._rescheduleLapse(card)
else:
self._rescheduleRev(card, ease, early)
@ -976,7 +973,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
card.lapses += 1
card.factor = max(1300, card.factor - 200)
suspended = self._checkLeech(card, conf) and card.queue == -1
suspended = self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED
if conf["delays"] and not suspended:
card.type = CARD_TYPE_RELEARNING
@ -987,7 +984,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
self._rescheduleAsRev(card, conf, early=False)
# need to reset the queue after rescheduling
if suspended:
card.queue = -1
card.queue = QUEUE_TYPE_SUSPENDED
delay = 0
return delay
@ -1047,11 +1044,11 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
else:
hardMin = 0
ivl2 = self._constrainedIvl(card.ivl * hardFactor, conf, hardMin, fuzz)
if ease == 2:
if ease == BUTTON_TWO:
return ivl2
ivl3 = self._constrainedIvl((card.ivl + delay // 2) * fct, conf, ivl2, fuzz)
if ease == 3:
if ease == BUTTON_THREE:
return ivl3
ivl4 = self._constrainedIvl(
@ -1101,7 +1098,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# next interval for card when answered early+correctly
def _earlyReviewIvl(self, card: Card, ease: int) -> int:
assert card.odid and card.type == 2
assert card.odid and card.type == CARD_TYPE_REV
assert card.factor
assert ease > 1
@ -1113,14 +1110,14 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
# early 3/4 reviews shouldn't decrease previous interval
minNewIvl = 1
if ease == 2:
if ease == BUTTON_TWO:
factor = conf.get("hardFactor", 1.2)
# hard cards shouldn't have their interval decreased by more than 50%
# of the normal factor
minNewIvl = factor / 2
elif ease == 3:
elif ease == BUTTON_THREE:
factor = card.factor / 1000
else: # ease == 4:
else: # ease == BUTTON_FOUR:
factor = card.factor / 1000
ease4 = conf["ease4"]
# 1.3 -> 1.15
@ -1213,7 +1210,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
t = "n.id desc"
elif o == DYN_DUEPRIORITY:
t = (
"(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)"
f"(case when queue={QUEUE_TYPE_REV} and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)"
% (self.today, self.today)
)
else: # DYN_DUE or unknown
@ -1231,7 +1228,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
queue = ""
if not deck["resched"]:
queue = ",queue=2"
queue = f",queue={QUEUE_TYPE_REV}"
query = (
"""
@ -1260,9 +1257,9 @@ where id = ?
# learning and relearning cards may be seconds-based or day-based;
# other types map directly to queues
if card.type in (1, CARD_TYPE_RELEARNING):
if card.type in (CARD_TYPE_LRN, CARD_TYPE_RELEARNING):
if card.odue > 1000000000:
card.queue = 1
card.queue = QUEUE_TYPE_LRN
else:
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
else:
@ -1284,8 +1281,8 @@ where id = ?
f.flush()
# handle
a = conf["leechAction"]
if a == 0:
card.queue = -1
if a == LEECH_SUSPEND:
card.queue = QUEUE_TYPE_SUSPENDED
# notify UI
hooks.card_did_leech(card)
return True
@ -1509,7 +1506,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
"True if there are any rev cards due."
return self.col.db.scalar(
(
"select 1 from cards where did in %s and queue = 2 "
f"select 1 from cards where did in %s and queue = {QUEUE_TYPE_REV} "
"and due <= ? limit 1"
)
% self._deckLimit(),
@ -1519,7 +1516,10 @@ To study outside of the normal schedule, click the Custom Study button below."""
def newDue(self) -> Any:
"True if there are any new cards due."
return self.col.db.scalar(
("select 1 from cards where did in %s and queue = 0 " "limit 1")
(
f"select 1 from cards where did in %s and queue = {QUEUE_TYPE_NEW} "
"limit 1"
)
% self._deckLimit()
)
@ -1557,14 +1557,14 @@ To study outside of the normal schedule, click the Custom Study button below."""
"Return the next interval for CARD, in seconds."
# preview mode?
if self._previewingCard(card):
if ease == 1:
if ease == BUTTON_ONE:
return self._previewDelay(card)
return 0
# (re)learning?
if card.queue in (0, 1, QUEUE_TYPE_DAY_LEARN_RELEARN):
if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
return self._nextLrnIvl(card, ease)
elif ease == 1:
elif ease == BUTTON_ONE:
# lapse
conf = self._lapseConf(card)
if conf["delays"]:
@ -1580,17 +1580,17 @@ To study outside of the normal schedule, click the Custom Study button below."""
# this isn't easily extracted from the learn code
def _nextLrnIvl(self, card: Card, ease: int) -> Any:
if card.queue == 0:
if card.queue == QUEUE_TYPE_NEW:
card.left = self._startingLeft(card)
conf = self._lrnConf(card)
if ease == 1:
if ease == BUTTON_ONE:
# fail
return self._delayForGrade(conf, len(conf["delays"]))
elif ease == 2:
elif ease == BUTTON_TWO:
return self._delayForRepeatingGrade(conf, card.left)
elif ease == 4:
elif ease == BUTTON_FOUR:
return self._graduatingIvl(card, conf, True, fuzz=False) * 86400
else: # ease == 3
else: # ease == BUTTON_THREE
left = card.left % 1000 - 1
if left <= 0:
# graduate
@ -1604,7 +1604,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
# learning and relearning cards may be seconds-based or day-based;
# other types map directly to queues
_restoreQueueSnippet = f"""
queue = (case when type in (1,{CARD_TYPE_RELEARNING}) then
queue = (case when type in ({CARD_TYPE_LRN},{CARD_TYPE_RELEARNING}) then
(case when (case when odue then odue else due end) > 1000000000 then 1 else
{QUEUE_TYPE_DAY_LEARN_RELEARN} end)
else
@ -1616,7 +1616,8 @@ end)
"Suspend cards."
self.col.log(ids)
self.col.db.execute(
"update cards set queue=-1,mod=?,usn=? where id in " + ids2str(ids),
f"update cards set queue={QUEUE_TYPE_SUSPENDED},mod=?,usn=? where id in "
+ ids2str(ids),
intTime(),
self.col.usn(),
)
@ -1625,7 +1626,9 @@ end)
"Unsuspend cards."
self.col.log(ids)
self.col.db.execute(
("update cards set %s,mod=?,usn=? " "where queue = -1 and id in %s")
(
f"update cards set %s,mod=?,usn=? where queue = {QUEUE_TYPE_SUSPENDED} and id in %s"
)
% (self._restoreQueueSnippet, ids2str(ids)),
intTime(),
self.col.usn(),
@ -1646,7 +1649,7 @@ update cards set queue=?,mod=?,usn=? where id in """
def buryNote(self, nid) -> None:
"Bury all cards for note until next session."
cids = self.col.db.list(
"select id from cards where nid = ? and queue >= 0", nid
f"select id from cards where nid = ? and queue >= {QUEUE_TYPE_NEW}", nid
)
self.buryCards(cids)
@ -1654,11 +1657,11 @@ update cards set queue=?,mod=?,usn=? where id in """
"Unbury all buried cards in all decks."
self.col.log(
self.col.db.list(
f"select id from cards where queue in (-2, {QUEUE_TYPE_MANUALLY_BURIED})"
f"select id from cards where queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})"
)
)
self.col.db.execute(
f"update cards set %s where queue in (-2, {QUEUE_TYPE_MANUALLY_BURIED})"
f"update cards set %s where queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})"
% self._restoreQueueSnippet
)
@ -1698,14 +1701,14 @@ update cards set queue=?,mod=?,usn=? where id in """
buryRev = rconf.get("bury", True)
# loop through and remove from queues
for cid, queue in self.col.db.execute(
"""
f"""
select id, queue from cards where nid=? and id!=?
and (queue=0 or (queue=2 and due<=?))""",
and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
card.nid,
card.id,
self.today,
):
if queue == 2:
if queue == QUEUE_TYPE_REV:
if buryRev:
toBury.append(cid)
# if bury disabled, we still discard to give same-day spacing
@ -1732,11 +1735,14 @@ and (queue=0 or (queue=2 and due<=?))""",
"Put cards at the end of the new queue."
self.remFromDyn(ids)
self.col.db.execute(
"update cards set type=0,queue=0,ivl=0,due=0,odue=0,factor=?"
f"update cards set type={CARD_TYPE_NEW},queue={QUEUE_TYPE_NEW},ivl=0,due=0,odue=0,factor=?"
" where id in " + ids2str(ids),
STARTING_FACTOR,
)
pmax = self.col.db.scalar("select max(due) from cards where type=0") or 0
pmax = (
self.col.db.scalar(f"select max(due) from cards where type={CARD_TYPE_NEW}")
or 0
)
# takes care of mod + usn
self.sortCards(ids, start=pmax + 1)
self.col.log(ids)
@ -1760,8 +1766,8 @@ and (queue=0 or (queue=2 and due<=?))""",
)
self.remFromDyn(ids)
self.col.db.executemany(
"""
update cards set type=2,queue=2,ivl=:ivl,due=:due,odue=0,
f"""
update cards set type={CARD_TYPE_REV},queue={QUEUE_TYPE_REV},ivl=:ivl,due=:due,odue=0,
usn=:usn,mod=:mod,factor=:fact where id=:id""",
d,
)
@ -1772,11 +1778,12 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
sids = ids2str(ids)
# we want to avoid resetting due number of existing new cards on export
nonNew = self.col.db.list(
"select id from cards where id in %s and (queue != 0 or type != 0)" % sids
f"select id from cards where id in %s and (queue != {QUEUE_TYPE_NEW} or type != {CARD_TYPE_NEW})"
% sids
)
# reset all cards
self.col.db.execute(
"update cards set reps=0,lapses=0,odid=0,odue=0,queue=0"
f"update cards set reps=0,lapses=0,odid=0,odue=0,queue={QUEUE_TYPE_NEW}"
" where id in %s" % sids
)
# and forget any non-new cards, changing their due numbers
@ -1817,16 +1824,16 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
# shift?
if shift:
low = self.col.db.scalar(
"select min(due) from cards where due >= ? and type = 0 "
f"select min(due) from cards where due >= ? and type = {CARD_TYPE_NEW} "
"and id not in %s" % scids,
start,
)
if low is not None:
shiftby = high - low + 1
self.col.db.execute(
"""
f"""
update cards set mod=?, usn=?, due=due+? where id not in %s
and due >= ? and queue = 0"""
and due >= ? and queue = {QUEUE_TYPE_NEW}"""
% scids,
now,
self.col.usn(),
@ -1836,7 +1843,7 @@ and due >= ? and queue = 0"""
# reorder cards
d = []
for id, nid in self.col.db.execute(
"select id, nid from cards where type = 0 and id in " + scids
f"select id, nid from cards where type = {CARD_TYPE_NEW} and id in " + scids
):
d.append(dict(now=now, due=due[nid], usn=self.col.usn(), cid=id))
self.col.db.executemany(
@ -1874,11 +1881,11 @@ and due >= ? and queue = 0"""
self.col.db.execute(
f"""
update cards set did = odid, queue = (case
when type = 1 then 0
when type = {CARD_TYPE_RELEARNING} then 2
when type = {CARD_TYPE_LRN} then {QUEUE_TYPE_NEW}
when type = {CARD_TYPE_RELEARNING} then {QUEUE_TYPE_REV}
else type end), type = (case
when type = 1 then 0
when type = {CARD_TYPE_RELEARNING} then 2
when type = {CARD_TYPE_LRN} then {CARD_TYPE_NEW}
when type = {CARD_TYPE_RELEARNING} then {CARD_TYPE_REV}
else type end),
due = odue, odue = 0, odid = 0, usn = ? where odid != 0""",
self.col.usn(),
@ -1890,8 +1897,8 @@ due = odue, odue = 0, odid = 0, usn = ? where odid != 0""",
self.col.db.execute(
f"""
update cards set
due = odue, queue = 2, type = 2, mod = %d, usn = %d, odue = 0
where queue in (1,{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type in (2, {CARD_TYPE_RELEARNING})
due = odue, queue = {QUEUE_TYPE_REV}, type = {CARD_TYPE_REV}, mod = %d, usn = %d, odue = 0
where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type in ({CARD_TYPE_REV}, {CARD_TYPE_RELEARNING})
"""
% (intTime(), self.col.usn())
)
@ -1899,15 +1906,15 @@ due = odue, odue = 0, odid = 0, usn = ? where odid != 0""",
self.col.db.execute(
f"""
update cards set
due = %d+ivl, queue = 2, type = 2, mod = %d, usn = %d, odue = 0
where queue in (1,{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type in (2, {CARD_TYPE_RELEARNING})
due = %d+ivl, queue = {QUEUE_TYPE_REV}, type = {CARD_TYPE_REV}, mod = %d, usn = %d, odue = 0
where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type in ({CARD_TYPE_REV}, {CARD_TYPE_RELEARNING})
"""
% (self.today, intTime(), self.col.usn())
)
# remove new cards from learning
self.forgetCards(
self.col.db.list(
f"select id from cards where queue in (1,{QUEUE_TYPE_DAY_LEARN_RELEARN})"
f"select id from cards where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN})"
)
)
@ -1916,13 +1923,13 @@ due = odue, odue = 0, odid = 0, usn = ? where odid != 0""",
self.col.db.execute(
f"""
update cards set type = (case
when type = 1 then 0
when type in (2, {CARD_TYPE_RELEARNING}) then 2
when type = {CARD_TYPE_LRN} then {CARD_TYPE_NEW}
when type in ({CARD_TYPE_REV}, {CARD_TYPE_RELEARNING}) then {CARD_TYPE_REV}
else type end),
due = (case when odue then odue else due end),
odue = 0,
mod = %d, usn = %d
where queue < 0"""
where queue < {QUEUE_TYPE_NEW}"""
% (intTime(), self.col.usn())
)
@ -1936,7 +1943,9 @@ where queue < 0"""
# adding 'hard' in v2 scheduler means old ease entries need shifting
# up or down
def _remapLearningAnswers(self, sql: str) -> None:
self.col.db.execute("update revlog set %s and type in (0,2)" % sql)
self.col.db.execute(
f"update revlog set %s and type in ({CARD_TYPE_NEW},{CARD_TYPE_REV})" % sql
)
def moveToV1(self) -> None:
self._emptyAllFiltered()

View file

@ -6,12 +6,17 @@ import json
import time
from typing import Any, Dict, List, Optional, Tuple
from anki.consts import *
from anki.lang import _, ngettext
from anki.utils import fmtTimeSpan, ids2str
# Card stats
##########################################################################
PERIOD_MONTH = 0
PERIOD_YEAR = 1
PERIOD_LIFE = 2
class CardStats:
def __init__(self, col, card) -> None:
@ -30,18 +35,18 @@ class CardStats:
if first:
self.addLine(_("First Review"), self.date(first / 1000))
self.addLine(_("Latest Review"), self.date(last / 1000))
if c.type in (1, 2):
if c.odid or c.queue < 0:
if c.type in (CARD_TYPE_LRN, CARD_TYPE_REV):
if c.odid or c.queue < QUEUE_TYPE_NEW:
next = None
else:
if c.queue in (2, 3):
if c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN):
next = time.time() + ((c.due - self.col.sched.today) * 86400)
else:
next = c.due
next = self.date(next)
if next:
self.addLine(_("Due"), next)
if c.queue == 2:
if c.queue == QUEUE_TYPE_REV:
self.addLine(_("Interval"), fmt(c.ivl * 86400))
self.addLine(_("Ease"), "%d%%" % (c.factor / 10.0))
self.addLine(_("Reviews"), "%d" % c.reps)
@ -52,7 +57,7 @@ class CardStats:
if cnt:
self.addLine(_("Average Time"), self.time(total / float(cnt)))
self.addLine(_("Total Time"), self.time(total))
elif c.queue == 0:
elif c.queue == QUEUE_TYPE_NEW:
self.addLine(_("Position"), c.due)
self.addLine(_("Card Type"), c.template()["name"])
self.addLine(_("Note Type"), c.model()["name"])
@ -102,14 +107,14 @@ class CollectionStats:
def __init__(self, col) -> None:
self.col = col
self._stats = None
self.type = 0
self.type = PERIOD_MONTH
self.width = 600
self.height = 200
self.wholeCollection = False
# assumes jquery & plot are available in document
def report(self, type=0) -> str:
# 0=days, 1=weeks, 2=months
def report(self, type=PERIOD_MONTH) -> str:
# 0=month, 1=year, 2=deck life
self.type = type
from .statsbg import bg
@ -149,13 +154,13 @@ body {background-image: url(data:image/png;base64,%s); }
if lim:
lim = " and " + lim
cards, thetime, failed, lrn, rev, relrn, filt = self.col.db.first(
"""
f"""
select count(), sum(time)/1000,
sum(case when ease = 1 then 1 else 0 end), /* failed */
sum(case when type = 0 then 1 else 0 end), /* learning */
sum(case when type = 1 then 1 else 0 end), /* review */
sum(case when type = 2 then 1 else 0 end), /* relearn */
sum(case when type = 3 then 1 else 0 end) /* filter */
sum(case when type = {REVLOG_LRN} then 1 else 0 end), /* learning */
sum(case when type = {REVLOG_REV} then 1 else 0 end), /* review */
sum(case when type = {REVLOG_RELRN} then 1 else 0 end), /* relearn */
sum(case when type = {REVLOG_CRAM} then 1 else 0 end) /* filter */
from revlog where id > ? """
+ lim,
(self.col.sched.dayCutoff - 86400) * 1000,
@ -215,9 +220,9 @@ from revlog where id > ? """
def get_start_end_chunk(self, by="review") -> Tuple[int, Optional[int], int]:
start = 0
if self.type == 0:
if self.type == PERIOD_MONTH:
end, chunk = 31, 1
elif self.type == 1:
elif self.type == PERIOD_YEAR:
end, chunk = 52, 7
else: # self.type == 2:
end = None
@ -279,8 +284,8 @@ from revlog where id > ? """
self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot)
self._line(i, _("Average"), self._avgDay(tot, num, _("reviews")))
tomorrow = self.col.db.scalar(
"""
select count() from cards where did in %s and queue in (2,3)
f"""
select count() from cards where did in %s and queue in ({QUEUE_TYPE_REV},{QUEUE_TYPE_DAY_LEARN_RELEARN})
and due = ?"""
% self._limit(),
self.col.sched.today + 1,
@ -296,12 +301,12 @@ and due = ?"""
if end is not None:
lim += " and day < %d" % end
return self.col.db.all(
"""
f"""
select (due-:today)/:chunk as day,
sum(case when ivl < 21 then 1 else 0 end), -- yng
sum(case when ivl >= 21 then 1 else 0 end) -- mtr
from cards
where did in %s and queue in (2,3)
where did in %s and queue in ({QUEUE_TYPE_REV},{QUEUE_TYPE_DAY_LEARN_RELEARN})
%s
group by day order by day"""
% (self._limit(), lim),
@ -396,7 +401,7 @@ group by day order by day"""
(10, colCram, _("Cram")),
),
)
if self.type == 0:
if self.type == PERIOD_MONTH:
t = _("Minutes")
convHours = False
else:
@ -513,7 +518,7 @@ group by day order by day"""
lim = "where " + " and ".join(lims)
else:
lim = ""
if self.type == 0:
if self.type == PERIOD_MONTH:
tf = 60.0 # minutes
else:
tf = 3600.0 # hours
@ -543,25 +548,25 @@ group by day order by day"""
lim = "where " + " and ".join(lims)
else:
lim = ""
if self.type == 0:
if self.type == PERIOD_MONTH:
tf = 60.0 # minutes
else:
tf = 3600.0 # hours
return self.col.db.all(
"""
f"""
select
(cast((id/1000.0 - :cut) / 86400.0 as int))/:chunk as day,
sum(case when type = 0 then 1 else 0 end), -- lrn count
sum(case when type = 1 and lastIvl < 21 then 1 else 0 end), -- yng count
sum(case when type = 1 and lastIvl >= 21 then 1 else 0 end), -- mtr count
sum(case when type = 2 then 1 else 0 end), -- lapse count
sum(case when type = 3 then 1 else 0 end), -- cram count
sum(case when type = 0 then time/1000.0 else 0 end)/:tf, -- lrn time
sum(case when type = {REVLOG_LRN} then 1 else 0 end), -- lrn count
sum(case when type = {REVLOG_REV} and lastIvl < 21 then 1 else 0 end), -- yng count
sum(case when type = {REVLOG_REV} and lastIvl >= 21 then 1 else 0 end), -- mtr count
sum(case when type = {REVLOG_RELRN} then 1 else 0 end), -- lapse count
sum(case when type = {REVLOG_CRAM} then 1 else 0 end), -- cram count
sum(case when type = {REVLOG_LRN} then time/1000.0 else 0 end)/:tf, -- lrn time
-- yng + mtr time
sum(case when type = 1 and lastIvl < 21 then time/1000.0 else 0 end)/:tf,
sum(case when type = 1 and lastIvl >= 21 then time/1000.0 else 0 end)/:tf,
sum(case when type = 2 then time/1000.0 else 0 end)/:tf, -- lapse time
sum(case when type = 3 then time/1000.0 else 0 end)/:tf -- cram time
sum(case when type = {REVLOG_REV} and lastIvl < 21 then time/1000.0 else 0 end)/:tf,
sum(case when type = {REVLOG_REV} and lastIvl >= 21 then time/1000.0 else 0 end)/:tf,
sum(case when type = {REVLOG_RELRN} then time/1000.0 else 0 end)/:tf, -- lapse time
sum(case when type = {REVLOG_CRAM} then time/1000.0 else 0 end)/:tf -- cram time
from revlog %s
group by day order by day"""
% lim,
@ -606,9 +611,9 @@ group by day order by day)"""
for (grp, cnt) in ivls:
tot += cnt
totd.append((grp, tot / float(all) * 100))
if self.type == 0:
if self.type == PERIOD_MONTH:
ivlmax = 31
elif self.type == 1:
elif self.type == PERIOD_YEAR:
ivlmax = 52
else:
ivlmax = max(5, ivls[-1][0])
@ -643,9 +648,9 @@ group by day order by day)"""
lim = "and grp <= %d" % end if end else ""
data = [
self.col.db.all(
"""
f"""
select ivl / :chunk as grp, count() from cards
where did in %s and queue = 2 %s
where did in %s and queue = {QUEUE_TYPE_REV} %s
group by grp
order by grp"""
% (self._limit(), lim),
@ -656,8 +661,8 @@ order by grp"""
data
+ list(
self.col.db.first(
"""
select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2"""
f"""
select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE_TYPE_REV}"""
% self._limit()
)
),
@ -675,9 +680,9 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2"""
types = ("lrn", "yng", "mtr")
eases = self._eases()
for (type, ease, cnt) in eases:
if type == 1:
if type == CARD_TYPE_LRN:
ease += 5
elif type == 2:
elif type == CARD_TYPE_REV:
ease += 10
n = types[type]
d[n].append((ease, cnt))
@ -714,7 +719,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2"""
return txt
def _easeInfo(self, eases) -> str:
types = {0: [0, 0], 1: [0, 0], 2: [0, 0]}
types = {PERIOD_MONTH: [0, 0], PERIOD_YEAR: [0, 0], PERIOD_LIFE: [0, 0]}
for (type, ease, cnt) in eases:
if ease == 1:
types[type][0] += cnt
@ -759,12 +764,12 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = 2"""
else:
ease4repl = "ease"
return self.col.db.all(
"""
f"""
select (case
when type in (0,2) then 0
when type in ({REVLOG_LRN},{REVLOG_RELRN}) then 0
when lastIvl < 21 then 1
else 2 end) as thetype,
(case when type in (0,2) and ease = 4 then %s else ease end), count() from revlog %s
(case when type in ({REVLOG_LRN},{REVLOG_RELRN}) and ease = 4 then %s else ease end), count() from revlog %s
group by thetype, ease
order by thetype, ease"""
% (ease4repl, lim)
@ -853,13 +858,13 @@ order by thetype, ease"""
if pd:
lim += " and id > %d" % ((self.col.sched.dayCutoff - (86400 * pd)) * 1000)
return self.col.db.all(
"""
f"""
select
23 - ((cast((:cut - id/1000) / 3600.0 as int)) %% 24) as hour,
sum(case when ease = 1 then 0 else 1 end) /
cast(count() as float) * 100,
count()
from revlog where type in (0,1,2) %s
from revlog where type in ({REVLOG_LRN},{REVLOG_REV},{REVLOG_RELRN}) %s
group by hour having count() > 30 order by hour"""
% lim,
cut=self.col.sched.dayCutoff - (rolloverHour * 3600),
@ -929,23 +934,23 @@ when you answer "good" on a review."""
def _factors(self) -> Any:
return self.col.db.first(
"""
f"""
select
min(factor) / 10.0,
avg(factor) / 10.0,
max(factor) / 10.0
from cards where did in %s and queue = 2"""
from cards where did in %s and queue = {QUEUE_TYPE_REV}"""
% self._limit()
)
def _cards(self) -> Any:
return self.col.db.first(
"""
f"""
select
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
sum(case when queue=0 then 1 else 0 end), -- new
sum(case when queue<0 then 1 else 0 end) -- susp
sum(case when queue={QUEUE_TYPE_REV} and ivl >= 21 then 1 else 0 end), -- mtr
sum(case when queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) or (queue={QUEUE_TYPE_REV} and ivl < 21) then 1 else 0 end), -- yng/lrn
sum(case when queue={QUEUE_TYPE_NEW} then 1 else 0 end), -- new
sum(case when queue<{QUEUE_TYPE_NEW} then 1 else 0 end) -- susp
from cards where did in %s"""
% self._limit()
)

View file

@ -111,7 +111,7 @@ def _upgrade(col, ver) -> None:
if ver < 3:
# new deck properties
for d in col.decks.all():
d["dyn"] = 0
d["dyn"] = DECK_STD
d["collapsed"] = False
col.decks.save(d)
if ver < 4:

View file

@ -1,6 +1,7 @@
# coding: utf-8
import pytest
from anki.consts import *
from anki.find import Finder
from tests.shared import getEmptyCol
@ -91,13 +92,13 @@ def test_findCards():
assert len(deck.findCards('"goats are"')) == 1
# card states
c = f.cards()[0]
c.queue = c.type = 2
c.queue = c.type = CARD_TYPE_REV
assert deck.findCards("is:review") == []
c.flush()
assert deck.findCards("is:review") == [c.id]
assert deck.findCards("is:due") == []
c.due = 0
c.queue = 2
c.queue = QUEUE_TYPE_REV
c.flush()
assert deck.findCards("is:due") == [c.id]
assert len(deck.findCards("-is:due")) == 4

View file

@ -4,7 +4,7 @@ import copy
import time
from anki import hooks
from anki.consts import STARTING_FACTOR
from anki.consts import *
from anki.utils import intTime
from tests.shared import getEmptyCol as getEmptyColOrig
@ -46,13 +46,13 @@ def test_new():
# fetch it
c = d.sched.getCard()
assert c
assert c.queue == 0
assert c.type == 0
assert c.queue == QUEUE_TYPE_NEW
assert c.type == CARD_TYPE_NEW
# if we answer it, it should become a learn card
t = intTime()
d.sched.answerCard(c, 1)
assert c.queue == 1
assert c.type == 1
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_LRN
assert c.due >= t
# disabled for now, as the learn fudging makes this randomly fail
@ -163,11 +163,11 @@ def test_learn():
assert c.left % 1000 == 1
assert c.left // 1000 == 1
# the next pass should graduate the card
assert c.queue == 1
assert c.type == 1
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_LRN
d.sched.answerCard(c, 2)
assert c.queue == 2
assert c.type == 2
assert c.queue == QUEUE_TYPE_REV
assert c.type == CARD_TYPE_REV
# should be due tomorrow, with an interval of 1
assert c.due == d.sched.today + 1
assert c.ivl == 1
@ -175,27 +175,27 @@ def test_learn():
c.type = 0
c.queue = 1
d.sched.answerCard(c, 3)
assert c.type == 2
assert c.queue == 2
assert c.type == CARD_TYPE_REV
assert c.queue == QUEUE_TYPE_REV
assert checkRevIvl(d, c, 4)
# revlog should have been updated each time
assert d.db.scalar("select count() from revlog where type = 0") == 5
# now failed card handling
c.type = 2
c.type = CARD_TYPE_REV
c.queue = 1
c.odue = 123
d.sched.answerCard(c, 3)
assert c.due == 123
assert c.type == 2
assert c.queue == 2
assert c.type == CARD_TYPE_REV
assert c.queue == QUEUE_TYPE_REV
# we should be able to remove manually, too
c.type = 2
c.type = CARD_TYPE_REV
c.queue = 1
c.odue = 321
c.flush()
d.sched.removeLrn()
c.load()
assert c.queue == 2
assert c.queue == QUEUE_TYPE_REV
assert c.due == 321
@ -247,7 +247,7 @@ def test_learn_day():
# answering it will place it in queue 3
d.sched.answerCard(c, 2)
assert c.due == d.sched.today + 1
assert c.queue == 3
assert c.queue == CARD_TYPE_RELEARNING
assert not d.sched.getCard()
# for testing, move it back a day
c.due -= 1
@ -259,7 +259,7 @@ def test_learn_day():
assert ni(c, 2) == 86400 * 2
# if we fail it, it should be back in the correct queue
d.sched.answerCard(c, 1)
assert c.queue == 1
assert c.queue == QUEUE_TYPE_LRN
d.undo()
d.reset()
c = d.sched.getCard()
@ -271,7 +271,7 @@ def test_learn_day():
# the last pass should graduate it into a review card
assert ni(c, 2) == 86400
d.sched.answerCard(c, 2)
assert c.queue == c.type == 2
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
# if the lapse step is tomorrow, failing it should handle the counts
# correctly
c.due = 0
@ -281,7 +281,7 @@ def test_learn_day():
d.sched._cardConf(c)["lapse"]["delays"] = [1440]
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.queue == 3
assert c.queue == CARD_TYPE_RELEARNING
assert d.sched.counts() == (0, 0, 0)
@ -294,8 +294,8 @@ def test_reviews():
d.addNote(f)
# set the card up as a review card, due 8 days ago
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = d.sched.today - 8
c.factor = STARTING_FACTOR
c.reps = 3
@ -311,7 +311,7 @@ def test_reviews():
d.reset()
d.sched._cardConf(c)["lapse"]["delays"] = [2, 20]
d.sched.answerCard(c, 1)
assert c.queue == 1
assert c.queue == QUEUE_TYPE_LRN
# it should be due tomorrow, with an interval of 1
assert c.odue == d.sched.today + 1
assert c.ivl == 1
@ -333,7 +333,7 @@ def test_reviews():
c = copy.copy(cardcopy)
c.flush()
d.sched.answerCard(c, 2)
assert c.queue == 2
assert c.queue == QUEUE_TYPE_REV
# the new interval should be (100 + 8/4) * 1.2 = 122
assert checkRevIvl(d, c, 122)
assert c.due == d.sched.today + c.ivl
@ -376,9 +376,9 @@ def test_reviews():
hooks.card_did_leech.append(onLeech)
d.sched.answerCard(c, 1)
assert hooked
assert c.queue == -1
assert c.queue == QUEUE_TYPE_SUSPENDED
c.load()
assert c.queue == -1
assert c.queue == QUEUE_TYPE_SUSPENDED
def test_button_spacing():
@ -388,8 +388,8 @@ def test_button_spacing():
d.addNote(f)
# 1 day ivl review card due now
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = d.sched.today
c.reps = 1
c.ivl = 1
@ -412,7 +412,7 @@ def test_overdue_lapse():
d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0]
c.type = 2
c.type = CARD_TYPE_REV
c.queue = 1
c.due = -1
c.odue = -1
@ -492,7 +492,7 @@ def test_nextIvl():
assert ni(c, 3) == 4 * 86400
# lapsed cards
##################################################
c.type = 2
c.type = CARD_TYPE_REV
c.ivl = 100
c.factor = STARTING_FACTOR
assert ni(c, 1) == 60
@ -500,7 +500,7 @@ def test_nextIvl():
assert ni(c, 3) == 100 * 86400
# review cards
##################################################
c.queue = 2
c.queue = QUEUE_TYPE_REV
c.ivl = 100
c.factor = STARTING_FACTOR
# failing it should put it at 60s
@ -551,20 +551,20 @@ def test_suspend():
# should cope with rev cards being relearnt
c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.flush()
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.due >= time.time()
assert c.queue == 1
assert c.type == 2
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_REV
d.sched.suspendCards([c.id])
d.sched.unsuspendCards([c.id])
c.load()
assert c.queue == 2
assert c.type == 2
assert c.queue == QUEUE_TYPE_REV
assert c.type == CARD_TYPE_REV
assert c.due == 1
# should cope with cards in cram decks
c.due = 1
@ -587,7 +587,8 @@ def test_cram():
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
c.type = c.queue = 2
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
# due in 25 days, so it's been waiting 75 days
c.due = d.sched.today + 25
c.mod = 1
@ -622,7 +623,7 @@ def test_cram():
# int(75*1.85) = 138
assert c.ivl == 138
assert c.odue == 138
assert c.queue == 1
assert c.queue == QUEUE_TYPE_LRN
# should be logged as a cram rep
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
# check ivls again
@ -634,7 +635,7 @@ def test_cram():
d.sched.answerCard(c, 2)
assert c.ivl == 138
assert c.due == 138
assert c.queue == 2
assert c.queue == QUEUE_TYPE_REV
# and it will have moved back to the previous deck
assert c.did == 1
# cram the deck again
@ -702,12 +703,12 @@ def test_cram_rem():
c = d.sched.getCard()
d.sched.answerCard(c, 2)
# answering the card will put it in the learning queue
assert c.type == c.queue == 1
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
assert c.due != oldDue
# if we terminate cramming prematurely it should be set back to new
d.sched.emptyDyn(did)
c.load()
assert c.type == c.queue == 0
assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW
assert c.due == oldDue
@ -731,10 +732,11 @@ def test_cram_resched():
assert ni(c, 3) == 0
assert d.sched.nextIvlStr(c, 3) == "(end)"
d.sched.answerCard(c, 3)
assert c.queue == c.type == 0
assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW
# undue reviews should also be unaffected
c.ivl = 100
c.type = c.queue = 2
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
c.due = d.sched.today + 25
c.factor = STARTING_FACTOR
c.flush()
@ -911,8 +913,8 @@ def test_repCounts():
f["Front"] = "three"
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = d.sched.today
c.flush()
d.reset()
@ -929,8 +931,8 @@ def test_timing():
f["Front"] = "num" + str(i)
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = 0
c.flush()
# fail the first one
@ -941,7 +943,7 @@ def test_timing():
d.sched.answerCard(c, 1)
# the next card should be another review
c = d.sched.getCard()
assert c.queue == 2
assert c.queue == QUEUE_TYPE_REV
# but if we wait for a second, the failed card should come back
orig_time = time.time
@ -950,7 +952,7 @@ def test_timing():
time.time = adjusted_time
c = d.sched.getCard()
assert c.queue == 1
assert c.queue == QUEUE_TYPE_LRN
time.time = orig_time
@ -982,7 +984,7 @@ def test_deckDue():
d.addNote(f)
# make it a review card
c = f.cards()[0]
c.queue = 2
c.queue = QUEUE_TYPE_REV
c.due = 0
c.flush()
# add one more with a new deck
@ -1100,8 +1102,8 @@ def test_forget():
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.queue = 2
c.type = 2
c.queue = QUEUE_TYPE_REV
c.type = CARD_TYPE_REV
c.ivl = 100
c.due = 0
c.flush()
@ -1122,7 +1124,7 @@ def test_resched():
c.load()
assert c.due == d.sched.today
assert c.ivl == 1
assert c.queue == c.type == 2
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
d.sched.reschedCards([c.id], 1, 1)
c.load()
assert c.due == d.sched.today + 1
@ -1136,8 +1138,8 @@ def test_norelearn():
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = 0
c.factor = STARTING_FACTOR
c.reps = 3
@ -1158,8 +1160,8 @@ def test_failmult():
f["Back"] = "two"
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.ivl = 100
c.due = d.sched.today - c.ivl
c.factor = STARTING_FACTOR

View file

@ -4,7 +4,7 @@ import copy
import time
from anki import hooks
from anki.consts import STARTING_FACTOR
from anki.consts import *
from anki.utils import intTime
from tests.shared import getEmptyCol as getEmptyColOrig
@ -57,13 +57,13 @@ def test_new():
# fetch it
c = d.sched.getCard()
assert c
assert c.queue == 0
assert c.type == 0
assert c.queue == QUEUE_TYPE_NEW
assert c.type == CARD_TYPE_NEW
# if we answer it, it should become a learn card
t = intTime()
d.sched.answerCard(c, 1)
assert c.queue == 1
assert c.type == 1
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_LRN
assert c.due >= t
# disabled for now, as the learn fudging makes this randomly fail
@ -176,11 +176,11 @@ def test_learn():
assert c.left % 1000 == 1
assert c.left // 1000 == 1
# the next pass should graduate the card
assert c.queue == 1
assert c.type == 1
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_LRN
d.sched.answerCard(c, 3)
assert c.queue == 2
assert c.type == 2
assert c.queue == QUEUE_TYPE_REV
assert c.type == CARD_TYPE_REV
# should be due tomorrow, with an interval of 1
assert c.due == d.sched.today + 1
assert c.ivl == 1
@ -188,8 +188,8 @@ def test_learn():
c.type = 0
c.queue = 1
d.sched.answerCard(c, 4)
assert c.type == 2
assert c.queue == 2
assert c.type == CARD_TYPE_REV
assert c.queue == QUEUE_TYPE_REV
assert checkRevIvl(d, c, 4)
# revlog should have been updated each time
assert d.db.scalar("select count() from revlog where type = 0") == 5
@ -203,20 +203,21 @@ def test_relearn():
c = f.cards()[0]
c.ivl = 100
c.due = d.sched.today
c.type = c.queue = 2
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
c.flush()
# fail the card
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.queue == 1
assert c.type == 3
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_RELEARNING
assert c.ivl == 1
# immediately graduate it
d.sched.answerCard(c, 4)
assert c.queue == c.type == 2
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
assert c.ivl == 2
assert c.due == d.sched.today + c.ivl
@ -229,7 +230,8 @@ def test_relearn_no_steps():
c = f.cards()[0]
c.ivl = 100
c.due = d.sched.today
c.type = c.queue = 2
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
c.flush()
conf = d.decks.confForDid(1)
@ -240,7 +242,7 @@ def test_relearn_no_steps():
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.type == c.queue == 2
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
def test_learn_collapsed():
@ -291,7 +293,7 @@ def test_learn_day():
# answering it will place it in queue 3
d.sched.answerCard(c, 3)
assert c.due == d.sched.today + 1
assert c.queue == 3
assert c.queue == QUEUE_TYPE_DAY_LEARN_RELEARN
assert not d.sched.getCard()
# for testing, move it back a day
c.due -= 1
@ -303,7 +305,7 @@ def test_learn_day():
assert ni(c, 3) == 86400 * 2
# if we fail it, it should be back in the correct queue
d.sched.answerCard(c, 1)
assert c.queue == 1
assert c.queue == QUEUE_TYPE_LRN
d.undo()
d.reset()
c = d.sched.getCard()
@ -315,7 +317,7 @@ def test_learn_day():
# the last pass should graduate it into a review card
assert ni(c, 3) == 86400
d.sched.answerCard(c, 3)
assert c.queue == c.type == 2
assert c.queue == CARD_TYPE_REV and c.type == QUEUE_TYPE_REV
# if the lapse step is tomorrow, failing it should handle the counts
# correctly
c.due = 0
@ -325,7 +327,7 @@ def test_learn_day():
d.sched._cardConf(c)["lapse"]["delays"] = [1440]
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.queue == 3
assert c.queue == QUEUE_TYPE_DAY_LEARN_RELEARN
assert d.sched.counts() == (0, 0, 0)
@ -338,8 +340,8 @@ def test_reviews():
d.addNote(f)
# set the card up as a review card, due 8 days ago
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = d.sched.today - 8
c.factor = STARTING_FACTOR
c.reps = 3
@ -355,7 +357,7 @@ def test_reviews():
c.flush()
d.reset()
d.sched.answerCard(c, 2)
assert c.queue == 2
assert c.queue == QUEUE_TYPE_REV
# the new interval should be (100) * 1.2 = 120
assert checkRevIvl(d, c, 120)
assert c.due == d.sched.today + c.ivl
@ -398,9 +400,9 @@ def test_reviews():
hooks.card_did_leech.append(onLeech)
d.sched.answerCard(c, 1)
assert hooked
assert c.queue == -1
assert c.queue == QUEUE_TYPE_SUSPENDED
c.load()
assert c.queue == -1
assert c.queue == QUEUE_TYPE_SUSPENDED
def test_review_limits():
@ -432,7 +434,8 @@ def test_review_limits():
# make them reviews
c = f.cards()[0]
c.queue = c.type = 2
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
c.due = 0
c.flush()
@ -474,8 +477,8 @@ def test_button_spacing():
d.addNote(f)
# 1 day ivl review card due now
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = d.sched.today
c.reps = 1
c.ivl = 1
@ -503,7 +506,7 @@ def test_overdue_lapse():
d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0]
c.type = 2
c.type = CARD_TYPE_REV
c.queue = 1
c.due = -1
c.odue = -1
@ -586,7 +589,7 @@ def test_nextIvl():
assert ni(c, 4) == 4 * 86400
# lapsed cards
##################################################
c.type = 2
c.type = CARD_TYPE_REV
c.ivl = 100
c.factor = STARTING_FACTOR
assert ni(c, 1) == 60
@ -594,7 +597,7 @@ def test_nextIvl():
assert ni(c, 4) == 101 * 86400
# review cards
##################################################
c.queue = 2
c.queue = QUEUE_TYPE_REV
c.ivl = 100
c.factor = STARTING_FACTOR
# failing it should put it at 60s
@ -624,25 +627,25 @@ def test_bury():
# burying
d.sched.buryCards([c.id], manual=True) # pylint: disable=unexpected-keyword-arg
c.load()
assert c.queue == -3
assert c.queue == QUEUE_TYPE_MANUALLY_BURIED
d.sched.buryCards([c2.id], manual=False) # pylint: disable=unexpected-keyword-arg
c2.load()
assert c2.queue == -2
assert c2.queue == QUEUE_TYPE_SIBLING_BURIED
d.reset()
assert not d.sched.getCard()
d.sched.unburyCardsForDeck(type="manual") # pylint: disable=unexpected-keyword-arg
c.load()
assert c.queue == 0
assert c.queue == QUEUE_TYPE_NEW
c2.load()
assert c2.queue == -2
assert c2.queue == QUEUE_TYPE_SIBLING_BURIED
d.sched.unburyCardsForDeck( # pylint: disable=unexpected-keyword-arg
type="siblings"
)
c2.load()
assert c2.queue == 0
assert c2.queue == QUEUE_TYPE_NEW
d.sched.buryCards([c.id, c2.id])
d.sched.unburyCardsForDeck(type="all") # pylint: disable=unexpected-keyword-arg
@ -671,21 +674,21 @@ def test_suspend():
# should cope with rev cards being relearnt
c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.flush()
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.due >= time.time()
due = c.due
assert c.queue == 1
assert c.type == 3
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_RELEARNING
d.sched.suspendCards([c.id])
d.sched.unsuspendCards([c.id])
c.load()
assert c.queue == 1
assert c.type == 3
assert c.queue == QUEUE_TYPE_LRN
assert c.type == CARD_TYPE_RELEARNING
assert c.due == due
# should cope with cards in cram decks
c.due = 1
@ -709,7 +712,8 @@ def test_filt_reviewing_early_normal():
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
c.type = c.queue = 2
c.queue = CARD_TYPE_REV
c.type = QUEUE_TYPE_REV
# due in 25 days, so it's been waiting 75 days
c.due = d.sched.today + 25
c.mod = 1
@ -740,7 +744,7 @@ def test_filt_reviewing_early_normal():
assert c.due == d.sched.today + c.ivl
assert not c.odue
# should not be in learning
assert c.queue == 2
assert c.queue == QUEUE_TYPE_REV
# should be logged as a cram rep
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
@ -771,11 +775,11 @@ def test_filt_keep_lrn_state():
d.sched.answerCard(c, 1)
assert c.type == c.queue == 1
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
assert c.left == 3003
d.sched.answerCard(c, 3)
assert c.type == c.queue == 1
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
# create a dynamic deck and refresh it
did = d.decks.newDyn("Cram")
@ -784,7 +788,7 @@ def test_filt_keep_lrn_state():
# card should still be in learning state
c.load()
assert c.type == c.queue == 1
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
assert c.left == 2002
# should be able to advance learning steps
@ -795,7 +799,7 @@ def test_filt_keep_lrn_state():
# emptying the deck preserves learning state
d.sched.emptyDyn(did)
c.load()
assert c.type == c.queue == 1
assert c.type == CARD_TYPE_LRN and c.queue == QUEUE_TYPE_LRN
assert c.left == 1001
assert c.due - intTime() > 60 * 60
@ -833,9 +837,9 @@ def test_preview():
# passing it will remove it
d.sched.answerCard(c2, 2)
assert c2.queue == 0
assert c2.queue == QUEUE_TYPE_NEW
assert c2.reps == 0
assert c2.type == 0
assert c2.type == CARD_TYPE_NEW
# the other card should appear again
c = d.sched.getCard()
@ -844,9 +848,9 @@ def test_preview():
# emptying the filtered deck should restore card
d.sched.emptyDyn(did)
c.load()
assert c.queue == 0
assert c.queue == QUEUE_TYPE_NEW
assert c.reps == 0
assert c.type == 0
assert c.type == CARD_TYPE_NEW
def test_ordcycle():
@ -943,8 +947,8 @@ def test_repCounts():
f["Front"] = "three"
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = d.sched.today
c.flush()
d.reset()
@ -961,8 +965,8 @@ def test_timing():
f["Front"] = "num" + str(i)
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = 0
c.flush()
# fail the first one
@ -971,13 +975,13 @@ def test_timing():
d.sched.answerCard(c, 1)
# the next card should be another review
c2 = d.sched.getCard()
assert c2.queue == 2
assert c2.queue == QUEUE_TYPE_REV
# if the failed card becomes due, it should show first
c.due = time.time() - 1
c.flush()
d.reset()
c = d.sched.getCard()
assert c.queue == 1
assert c.queue == QUEUE_TYPE_LRN
def test_collapse():
@ -1008,7 +1012,7 @@ def test_deckDue():
d.addNote(f)
# make it a review card
c = f.cards()[0]
c.queue = 2
c.queue = QUEUE_TYPE_REV
c.due = 0
c.flush()
# add one more with a new deck
@ -1126,8 +1130,8 @@ def test_forget():
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.queue = 2
c.type = 2
c.queue = QUEUE_TYPE_REV
c.type = CARD_TYPE_REV
c.ivl = 100
c.due = 0
c.flush()
@ -1148,7 +1152,7 @@ def test_resched():
c.load()
assert c.due == d.sched.today
assert c.ivl == 1
assert c.queue == c.type == 2
assert c.queue == QUEUE_TYPE_REV and c.type == CARD_TYPE_REV
d.sched.reschedCards([c.id], 1, 1)
c.load()
assert c.due == d.sched.today + 1
@ -1162,8 +1166,8 @@ def test_norelearn():
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.due = 0
c.factor = STARTING_FACTOR
c.reps = 3
@ -1184,8 +1188,8 @@ def test_failmult():
f["Back"] = "two"
d.addNote(f)
c = f.cards()[0]
c.type = 2
c.queue = 2
c.type = CARD_TYPE_REV
c.queue = QUEUE_TYPE_REV
c.ivl = 100
c.due = d.sched.today - c.ivl
c.factor = STARTING_FACTOR
@ -1217,8 +1221,8 @@ def test_moveVersions():
# the move to v2 should reset it to new
col.changeSchedulerVer(2)
c.load()
assert c.queue == 0
assert c.type == 0
assert c.queue == QUEUE_TYPE_NEW
assert c.type == CARD_TYPE_NEW
# fail it again, and manually bury it
col.reset()
@ -1226,19 +1230,19 @@ def test_moveVersions():
col.sched.answerCard(c, 1)
col.sched.buryCards([c.id])
c.load()
assert c.queue == -3
assert c.queue == QUEUE_TYPE_MANUALLY_BURIED
# revert to version 1
col.changeSchedulerVer(1)
# card should have moved queues
c.load()
assert c.queue == -2
assert c.queue == QUEUE_TYPE_SIBLING_BURIED
# and it should be new again when unburied
col.sched.unburyCards()
c.load()
assert c.queue == c.type == 0
assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW
# make sure relearning cards transition correctly to v1
col.changeSchedulerVer(2)
@ -1269,7 +1273,7 @@ def test_negativeDueFilter():
d.addNote(f)
c = f.cards()[0]
c.due = -5
c.queue = 2
c.queue = QUEUE_TYPE_REV
c.ivl = 5
c.flush()

View file

@ -55,18 +55,18 @@ def test_review():
# answer
assert d.sched.counts() == (1, 0, 0)
c = d.sched.getCard()
assert c.queue == 0
assert c.queue == QUEUE_TYPE_NEW
d.sched.answerCard(c, 3)
assert c.left == 1001
assert d.sched.counts() == (0, 1, 0)
assert c.queue == 1
assert c.queue == QUEUE_TYPE_LRN
# undo
assert d.undoName()
d.undo()
d.reset()
assert d.sched.counts() == (1, 0, 0)
c.load()
assert c.queue == 0
assert c.queue == QUEUE_TYPE_NEW
assert c.left != 1001
assert not d.undoName()
# we should be able to undo multiple answers too

View file

@ -67,6 +67,16 @@ hooks = [
Your add-on can check filter_name to decide whether it should modify
field_text or not before returning it.""",
),
Hook(
name="note_will_flush",
args=["note: Note"],
doc="Allow to change a note before it is added/updated in the database.",
),
Hook(
name="card_will_flush",
args=["card: Card"],
doc="Allow to change a card before it is added/updated in the database.",
),
Hook(
name="card_did_render",
args=[

View file

@ -219,5 +219,5 @@ suggestions, bug reports and donations."
abt.label.setMinimumWidth(800)
abt.label.setMinimumHeight(600)
dialog.show()
abt.label.stdHtml(abouttext, js=" ")
abt.label.stdHtml(abouttext, js=[])
return dialog

View file

@ -350,11 +350,13 @@ class DataModel(QAbstractTableModel):
def nextDue(self, c, index):
if c.odid:
return _("(filtered)")
elif c.queue == 1:
elif c.queue == QUEUE_TYPE_LRN:
date = c.due
elif c.queue == 0 or c.type == 0:
elif c.queue == QUEUE_TYPE_NEW or c.type == CARD_TYPE_NEW:
return str(c.due)
elif c.queue in (2, 3) or (c.type == 2 and c.queue < 0):
elif c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN) or (
c.type == CARD_TYPE_REV and c.queue < 0
):
date = time.time() + ((c.due - self.col.sched.today) * 86400)
else:
return ""
@ -1382,27 +1384,20 @@ by clicking on one on the left."""
info, cs = self._cardInfoData()
reps = self._revlogData(cs)
class CardInfoDialog(QDialog):
silentlyClose = True
def reject(self):
saveGeom(self, "revlog")
return QDialog.reject(self)
d = CardInfoDialog(self)
card_info_dialog = CardInfoDialog(self)
l = QVBoxLayout()
l.setContentsMargins(0, 0, 0, 0)
w = AnkiWebView()
w = AnkiWebView(title="browser card info")
l.addWidget(w)
w.stdHtml(info + "<p>" + reps)
w.stdHtml(info + "<p>" + reps, context=card_info_dialog)
bb = QDialogButtonBox(QDialogButtonBox.Close)
l.addWidget(bb)
bb.rejected.connect(d.reject)
d.setLayout(l)
d.setWindowModality(Qt.WindowModal)
d.resize(500, 400)
restoreGeom(d, "revlog")
d.show()
bb.rejected.connect(card_info_dialog.reject)
card_info_dialog.setLayout(l)
card_info_dialog.setWindowModality(Qt.WindowModal)
card_info_dialog.resize(500, 400)
restoreGeom(card_info_dialog, "revlog", CardInfoDialog)
card_info_dialog.show()
def _cardInfoData(self):
from anki.stats import CardStats
@ -1446,9 +1441,9 @@ border: 1px solid #000; padding: 3px; '>%s</div>"""
import anki.stats as st
fmt = "<span style='color:%s'>%s</span>"
if type == 0:
if type == CARD_TYPE_NEW:
tstr = fmt % (st.colLearn, tstr)
elif type == 1:
elif type == CARD_TYPE_LRN:
tstr = fmt % (st.colMature, tstr)
elif type == 2:
tstr = fmt % (st.colRelearn, tstr)
@ -1561,7 +1556,7 @@ where id in %s"""
self._previewWindow.silentlyClose = True
vbox = QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0)
self._previewWeb = AnkiWebView()
self._previewWeb = AnkiWebView(title="previewer")
vbox.addWidget(self._previewWeb)
bbox = QDialogButtonBox()
@ -1656,12 +1651,15 @@ where id in %s"""
"mathjax/MathJax.js",
"reviewer.js",
]
web_context = PreviewDialog(dialog=self._previewWindow, browser=self)
self._previewWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
self.mw.reviewer.revHtml(),
css=["reviewer.css"],
js=jsinc,
context=web_context,
)
self._previewWeb.set_bridge_command(
self._on_preview_bridge_cmd,
PreviewDialog(dialog=self._previewWindow, browser=self),
self._on_preview_bridge_cmd, web_context,
)
def _on_preview_bridge_cmd(self, cmd: str) -> Any:
@ -1737,7 +1735,7 @@ where id in %s"""
av_player.play_tags(audio)
txt = self.mw.prepare_card_text_for_display(txt)
gui_hooks.card_will_show(
txt = gui_hooks.card_will_show(
txt, c, "preview" + self._previewState.capitalize()
)
self._lastPreviewState = self._previewStateAndMod()
@ -1965,7 +1963,8 @@ update cards set usn=?, mod=?, did=? where id in """
def _reposition(self):
cids = self.selectedCards()
cids2 = self.col.db.list(
"select id from cards where type = 0 and id in " + ids2str(cids)
f"select id from cards where type = {CARD_TYPE_NEW} and id in "
+ ids2str(cids)
)
if not cids2:
return showInfo(_("Only new cards can be repositioned."))
@ -1974,7 +1973,7 @@ update cards set usn=?, mod=?, did=? where id in """
frm = aqt.forms.reposition.Ui_Dialog()
frm.setupUi(d)
(pmin, pmax) = self.col.db.first(
"select min(due), max(due) from cards where type=0 and odid=0"
f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0"
)
pmin = pmin or 0
pmax = pmax or 0
@ -2158,10 +2157,10 @@ update cards set usn=?, mod=?, did=? where id in """
frm.fields.addItems(fields)
self._dupesButton = None
# links
frm.webView.set_bridge_command(
self.dupeLinkClicked, FindDupesDialog(dialog=d, browser=self)
)
frm.webView.stdHtml("")
frm.webView.title = "find duplicates"
web_context = FindDupesDialog(dialog=d, browser=self)
frm.webView.set_bridge_command(self.dupeLinkClicked, web_context)
frm.webView.stdHtml("", context=web_context)
def onFin(code):
saveGeom(d, "findDupes")
@ -2170,13 +2169,15 @@ update cards set usn=?, mod=?, did=? where id in """
def onClick():
field = fields[frm.fields.currentIndex()]
self.duplicatesReport(frm.webView, field, frm.search.text(), frm)
self.duplicatesReport(
frm.webView, field, frm.search.text(), frm, web_context
)
search = frm.buttonBox.addButton(_("Search"), QDialogButtonBox.ActionRole)
search.clicked.connect(onClick)
d.show()
def duplicatesReport(self, web, fname, search, frm):
def duplicatesReport(self, web, fname, search, frm, web_context):
self.mw.progress.start()
res = self.mw.col.findDupes(fname, search)
if not self._dupesButton:
@ -2201,7 +2202,7 @@ update cards set usn=?, mod=?, did=? where id in """
)
)
t += "</ol>"
web.stdHtml(t)
web.stdHtml(t, context=web_context)
self.mw.progress.finish()
def _onTagDupes(self, res):
@ -2475,3 +2476,19 @@ Are you sure you want to continue?"""
def onHelp(self):
openHelp("browsermisc")
# Card Info Dialog
######################################################################
class CardInfoDialog(QDialog):
silentlyClose = True
def __init__(self, browser: Browser, *args, **kwargs):
super().__init__(browser, *args, **kwargs)
self.browser = browser
def reject(self):
saveGeom(self, "revlog")
return QDialog.reject(self)

View file

@ -203,9 +203,9 @@ class CardLayout(QDialog):
def setupWebviews(self):
pform = self.pform
pform.frontWeb = AnkiWebView()
pform.frontWeb = AnkiWebView(title="card layout front")
pform.frontPrevBox.addWidget(pform.frontWeb)
pform.backWeb = AnkiWebView()
pform.backWeb = AnkiWebView(title="card layout back")
pform.backPrevBox.addWidget(pform.backWeb)
jsinc = [
"jquery.js",
@ -215,10 +215,10 @@ class CardLayout(QDialog):
"reviewer.js",
]
pform.frontWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
)
pform.backWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc, context=self,
)
pform.frontWeb.set_bridge_command(self._on_bridge_cmd, self)
pform.backWeb.set_bridge_command(self._on_bridge_cmd, self)

View file

@ -32,17 +32,17 @@ class CustomStudy(QDialog):
f.setupUi(self)
self.setWindowModality(Qt.WindowModal)
self.setupSignals()
f.radio1.click()
f.radioNew.click()
self.exec_()
def setupSignals(self):
f = self.form
f.radio1.clicked.connect(lambda: self.onRadioChange(1))
f.radio2.clicked.connect(lambda: self.onRadioChange(2))
f.radio3.clicked.connect(lambda: self.onRadioChange(3))
f.radio4.clicked.connect(lambda: self.onRadioChange(4))
f.radio5.clicked.connect(lambda: self.onRadioChange(5))
f.radio6.clicked.connect(lambda: self.onRadioChange(6))
f.radioNew.clicked.connect(lambda: self.onRadioChange(RADIO_NEW))
f.radioRev.clicked.connect(lambda: self.onRadioChange(RADIO_REV))
f.radioForgot.clicked.connect(lambda: self.onRadioChange(RADIO_FORGOT))
f.radioAhead.clicked.connect(lambda: self.onRadioChange(RADIO_AHEAD))
f.radioPreview.clicked.connect(lambda: self.onRadioChange(RADIO_PREVIEW))
f.radioCram.clicked.connect(lambda: self.onRadioChange(RADIO_CRAM))
def onRadioChange(self, idx):
f = self.form

View file

@ -109,6 +109,7 @@ class DeckBrowser:
self._body % dict(tree=tree, stats=stats, countwarn=self._countWarn()),
css=["deckbrowser.css"],
js=["jquery.js", "jquery-ui.js", "deckbrowser.js"],
context=self,
)
self.web.key = "deckBrowser"
self._drawButtons()
@ -340,9 +341,10 @@ where id > ?""",
<button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple(
b
)
self.bottom.draw(buf)
self.bottom.web.set_bridge_command(
self._linkHandler, DeckBrowserBottomBar(self)
self.bottom.draw(
buf=buf,
link_handler=self._linkHandler,
web_context=DeckBrowserBottomBar(self),
)
def _onShared(self):

View file

@ -95,7 +95,6 @@ class Editor:
def setupWeb(self) -> None:
self.web = EditorWebView(self.widget, self)
self.web.title = "editor"
self.web.allowDrops = True
self.web.set_bridge_command(self.onBridgeCmd, self)
self.outerLayout.addWidget(self.web, 1)
@ -167,6 +166,7 @@ class Editor:
_html % (bgcol, bgcol, topbuts, _("Show Duplicates")),
css=["editor.css"],
js=["jquery.js", "editor.js"],
context=self,
)
# Top buttons
@ -937,7 +937,7 @@ to a cloze type first, via Edit>Change Note Type."""
class EditorWebView(AnkiWebView):
def __init__(self, parent, editor):
AnkiWebView.__init__(self)
AnkiWebView.__init__(self, title="editor")
self.editor = editor
self.strip = self.editor.mw.pm.profile["stripHTML"]
self.setAcceptDrops(True)

View file

@ -7,7 +7,7 @@ See pylib/anki/hooks.py
from __future__ import annotations
from typing import Any, Callable, Dict, List, Tuple
from typing import Any, Callable, Dict, List, Optional, Tuple
import anki
import aqt
@ -1161,6 +1161,67 @@ class _WebviewDidReceiveJsMessageFilter:
webview_did_receive_js_message = _WebviewDidReceiveJsMessageFilter()
class _WebviewWillSetContentHook:
"""Used to modify web content before it is rendered.
Web_content contains the HTML, JS, and CSS the web view will be
populated with.
Context is the instance that was passed to stdHtml().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
def on_webview_will_set_content(web_content: WebContent, context):
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
# reviewer, perform changes to content
context: aqt.reviewer.Reviewer
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
web_content.head += "<script>console.log('my-addon')</script>"
web_content.body += "<div id='my-addon'></div>"
"""
_hooks: List[Callable[["aqt.webview.WebContent", Optional[Any]], None]] = []
def append(
self, cb: Callable[["aqt.webview.WebContent", Optional[Any]], None]
) -> None:
"""(web_content: aqt.webview.WebContent, context: Optional[Any])"""
self._hooks.append(cb)
def remove(
self, cb: Callable[["aqt.webview.WebContent", Optional[Any]], None]
) -> None:
if cb in self._hooks:
self._hooks.remove(cb)
def __call__(
self, web_content: aqt.webview.WebContent, context: Optional[Any]
) -> None:
for hook in self._hooks:
try:
hook(web_content, context)
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
webview_will_set_content = _WebviewWillSetContentHook()
class _WebviewWillShowContextMenuHook:
_hooks: List[Callable[["aqt.webview.AnkiWebView", QMenu], None]] = []

View file

@ -663,9 +663,8 @@ from the profile screen."
if self.resetModal:
# we don't have to change the webview, as we have a covering window
return
self.web.set_bridge_command(
lambda url: self.delayedMaybeReset(), ResetRequired(self)
)
web_context = ResetRequired(self)
self.web.set_bridge_command(lambda url: self.delayedMaybeReset(), web_context)
i = _("Waiting for editing to finish.")
b = self.button("refresh", _("Resume Now"), id="resume")
self.web.stdHtml(
@ -676,7 +675,8 @@ from the profile screen."
%s</div></div></center>
<script>$('#resume').focus()</script>
"""
% (i, b)
% (i, b),
context=web_context,
)
self.bottomWeb.hide()
self.web.setFocus()
@ -717,19 +717,16 @@ title="%s" %s>%s</button>""" % (
self.form = aqt.forms.main.Ui_MainWindow()
self.form.setupUi(self)
# toolbar
tweb = self.toolbarWeb = aqt.webview.AnkiWebView()
tweb.title = "top toolbar"
tweb = self.toolbarWeb = aqt.webview.AnkiWebView(title="top toolbar")
tweb.setFocusPolicy(Qt.WheelFocus)
self.toolbar = aqt.toolbar.Toolbar(self, tweb)
self.toolbar.draw()
# main area
self.web = aqt.webview.AnkiWebView()
self.web.title = "main webview"
self.web = aqt.webview.AnkiWebView(title="main webview")
self.web.setFocusPolicy(Qt.WheelFocus)
self.web.setMinimumWidth(400)
# bottom area
sweb = self.bottomWeb = aqt.webview.AnkiWebView()
sweb.title = "bottom toolbar"
sweb = self.bottomWeb = aqt.webview.AnkiWebView(title="bottom toolbar")
sweb.setFocusPolicy(Qt.WheelFocus)
# add in a layout
self.mainLayout = QVBoxLayout()

View file

@ -151,6 +151,7 @@ class Overview:
),
css=["overview.css"],
js=["jquery.js", "overview.js"],
context=self,
)
def _desc(self, deck):
@ -243,8 +244,9 @@ to their original deck."""
<button title="%s" onclick='pycmd("%s")'>%s</button>""" % tuple(
b
)
self.bottom.draw(buf)
self.bottom.web.set_bridge_command(self._linkHandler, OverviewBottomBar(self))
self.bottom.draw(
buf=buf, link_handler=self._linkHandler, web_context=OverviewBottomBar(self)
)
# Studying more
######################################################################

View file

@ -153,6 +153,7 @@ class Reviewer:
"mathjax/MathJax.js",
"reviewer.js",
],
context=self,
)
# show answer / ease buttons
self.bottom.web.show()
@ -160,6 +161,7 @@ class Reviewer:
self._bottomHTML(),
css=["toolbar-bottom.css", "reviewer-bottom.css"],
js=["jquery.js", "reviewer-bottom.js"],
context=ReviewerBottomBar(self),
)
# Showing the question

View file

@ -95,7 +95,10 @@ class DeckStats(QDialog):
stats = self.mw.col.stats()
stats.wholeCollection = self.wholeCollection
self.report = stats.report(type=self.period)
self.form.web.title = "deck stats"
self.form.web.stdHtml(
"<html><body>" + self.report + "</body></html>", js=["jquery.js", "plot.js"]
"<html><body>" + self.report + "</body></html>",
js=["jquery.js", "plot.js"],
context=self,
)
self.mw.progress.finish()

View file

@ -4,7 +4,7 @@
from __future__ import annotations
from typing import Dict
from typing import Any, Dict, Optional
import aqt
from anki.lang import _
@ -40,9 +40,18 @@ class Toolbar:
self.web.setFixedHeight(30)
self.web.requiresCol = False
def draw(self):
self.web.set_bridge_command(self._linkHandler, TopToolbar(self))
self.web.stdHtml(self._body % self._centerLinks(), css=["toolbar.css"])
def draw(
self,
buf: str = "",
web_context: Optional[Any] = None,
link_handler: Optional[Callable[[str], Any]] = None,
):
web_context = web_context or TopToolbar(self)
link_handler = link_handler or self._linkHandler
self.web.set_bridge_command(link_handler, web_context)
self.web.stdHtml(
self._body % self._centerLinks(), css=["toolbar.css"], context=web_context,
)
self.web.adjustHeightToFit()
# Available links
@ -133,10 +142,19 @@ class BottomBar(Toolbar):
%s</td></tr></table></center>
"""
def draw(self, buf):
def draw(
self,
buf: str = "",
web_context: Optional[Any] = None,
link_handler: Optional[Callable[[str], Any]] = None,
):
# note: some screens may override this
self.web.set_bridge_command(self._linkHandler, BottomToolbar(self))
web_context = web_context or BottomToolbar(self)
link_handler = link_handler or self._linkHandler
self.web.set_bridge_command(link_handler, web_context)
self.web.stdHtml(
self._centerBody % buf, css=["toolbar.css", "toolbar-bottom.css"]
self._centerBody % buf,
css=["toolbar.css", "toolbar-bottom.css"],
context=web_context,
)
self.web.adjustHeightToFit()

View file

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import dataclasses
import json
import math
import sys
@ -96,14 +97,76 @@ class AnkiWebPage(QWebEnginePage): # type: ignore
return self._onBridgeCmd(str)
# Add-ons
##########################################################################
@dataclasses.dataclass
class WebContent:
"""Stores all dynamically modified content that a particular web view
will be populated with.
Attributes:
body {str} -- HTML body
head {str} -- HTML head
css {List[str]} -- List of media server subpaths,
each pointing to a CSS file
js {List[str]} -- List of media server subpaths,
each pointing to a JS file
Important Notes:
- When modifying the attributes specified above, please make sure your
changes only perform the minimum requried edits to make your add-on work.
You should avoid overwriting or interfering with existing data as much
as possible, instead opting to append your own changes, e.g.:
def on_webview_will_set_content(web_content: WebContent, context):
web_content.body += "<my_html>"
web_content.head += "<my_head>"
- The paths specified in `css` and `js` need to be accessible by Anki's
media server. All list members without a specified subpath are assumed
to be located under `/_anki`, which is the media server subpath used
for all web assets shipped with Anki.
Add-ons may expose their own web assets by utilizing
aqt.addons.AddonManager.setWebExports(). Web exports registered
in this manner may then be accessed under the `/_addons` subpath.
E.g., to allow access to a `my-addon.js` and `my-addon.css` residing
in a "web" subfolder in your add-on package, first register the
corresponding web export:
> from aqt import mw
> mw.addonManager.setWebExports(__name__, r"web/.*(css|js)")
Then append the subpaths to the corresponding web_content fields
within a function subscribing to gui_hooks.webview_will_set_content:
def on_webview_will_set_content(web_content: WebContent, context):
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
"""
body: str = ""
head: str = ""
css: List[str] = dataclasses.field(default_factory=lambda: [])
js: List[str] = dataclasses.field(default_factory=lambda: [])
# Main web view
##########################################################################
class AnkiWebView(QWebEngineView): # type: ignore
def __init__(self, parent: Optional[QWidget] = None) -> None:
def __init__(
self, parent: Optional[QWidget] = None, title: str = "default"
) -> None:
QWebEngineView.__init__(self, parent=parent) # type: ignore
self.title = "default"
self.title = title
self._page = AnkiWebPage(self._onBridgeCmd)
self._page.setBackgroundColor(self._getWindowColor()) # reduce flicker
@ -254,11 +317,23 @@ class AnkiWebView(QWebEngineView): # type: ignore
return QColor("#ececec")
return self.style().standardPalette().color(QPalette.Window)
def stdHtml(self, body, css=None, js=None, head=""):
if css is None:
css = []
if js is None:
js = ["jquery.js"]
def stdHtml(
self,
body: str,
css: Optional[List[str]] = None,
js: Optional[List[str]] = None,
head: str = "",
context: Optional[Any] = None,
):
web_content = WebContent(
body=body,
head=head,
js=["webview.js"] + (["jquery.js"] if js is None else js),
css=["webview.css"] + ([] if css is None else css),
)
gui_hooks.webview_will_set_content(web_content, context)
palette = self.style().standardPalette()
color_hl = palette.color(QPalette.Highlight).name()
@ -299,16 +374,12 @@ div[contenteditable="true"]:focus {
"color_hl_txt": color_hl_txt,
}
csstxt = "\n".join(
[self.bundledCSS("webview.css")] + [self.bundledCSS(fname) for fname in css]
)
jstxt = "\n".join(
[self.bundledScript("webview.js")]
+ [self.bundledScript(fname) for fname in js]
)
csstxt = "\n".join(self.bundledCSS(fname) for fname in web_content.css)
jstxt = "\n".join(self.bundledScript(fname) for fname in web_content.js)
from aqt import mw
head = mw.baseHTML() + head + csstxt + jstxt
head = mw.baseHTML() + web_content.head + csstxt + jstxt
body_class = theme_manager.body_class()
@ -334,20 +405,25 @@ body {{ zoom: {}; background: {}; {} }}
widgetspec,
head,
body_class,
body,
web_content.body,
)
# print(html)
self.setHtml(html)
def webBundlePath(self, path):
def webBundlePath(self, path: str) -> str:
from aqt import mw
return "http://127.0.0.1:%d/_anki/%s" % (mw.mediaServer.getPort(), path)
if path.startswith("/"):
subpath = ""
else:
subpath = "/_anki/"
def bundledScript(self, fname):
return f"http://127.0.0.1:{mw.mediaServer.getPort()}{subpath}{path}"
def bundledScript(self, fname: str) -> str:
return '<script src="%s"></script>' % self.webBundlePath(fname)
def bundledCSS(self, fname):
def bundledCSS(self, fname: str) -> str:
return '<link rel="stylesheet" type="text/css" href="%s">' % self.webBundlePath(
fname
)

View file

@ -17,42 +17,42 @@
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<widget class="QRadioButton" name="radio4">
<widget class="QRadioButton" name="radioAhead">
<property name="text">
<string>Review ahead</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="radio3">
<widget class="QRadioButton" name="radioForgot">
<property name="text">
<string>Review forgotten cards</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="radio1">
<widget class="QRadioButton" name="radioNew">
<property name="text">
<string>Increase today's new card limit</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="radio2">
<widget class="QRadioButton" name="radioRev">
<property name="text">
<string>Increase today's review card limit</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QRadioButton" name="radio6">
<widget class="QRadioButton" name="radioCram">
<property name="text">
<string>Study by card state or tag</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QRadioButton" name="radio5">
<widget class="QRadioButton" name="radioPreview">
<property name="text">
<string>Preview new cards</string>
</property>
@ -163,11 +163,12 @@
</layout>
</widget>
<tabstops>
<tabstop>radio1</tabstop>
<tabstop>radio2</tabstop>
<tabstop>radio3</tabstop>
<tabstop>radio4</tabstop>
<tabstop>radio6</tabstop>
<tabstop>radioNew</tabstop>
<tabstop>radioRev</tabstop>
<tabstop>radioForgot</tabstop>
<tabstop>radioAhead</tabstop>
<tabstop>radioPreview</tabstop>
<tabstop>radioCram</tabstop>
<tabstop>spin</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>

View file

@ -163,6 +163,40 @@ hooks = [
return handled
""",
),
Hook(
name="webview_will_set_content",
args=["web_content: aqt.webview.WebContent", "context: Optional[Any]",],
doc="""Used to modify web content before it is rendered.
Web_content contains the HTML, JS, and CSS the web view will be
populated with.
Context is the instance that was passed to stdHtml().
It can be inspected to check which screen this hook is firing
in, and to get a reference to the screen. For example, if your
code wishes to function only in the review screen, you could do:
def on_webview_will_set_content(web_content: WebContent, context):
if not isinstance(context, aqt.reviewer.Reviewer):
# not reviewer, do not modify content
return
# reviewer, perform changes to content
context: aqt.reviewer.Reviewer
addon_package = mw.addonManager.addonFromModule(__name__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
web_content.js.append(
f"/_addons/{addon_package}/web/my-addon.js")
web_content.head += "<script>console.log('my-addon')</script>"
web_content.body += "<div id='my-addon'></div>"
""",
),
Hook(
name="webview_will_show_context_menu",
args=["webview: aqt.webview.AnkiWebView", "menu: QMenu"],