mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00

The type name needs to be quoted so it is evaluated after the module has been initialized. In the future when we drop support for python 3.6, and mypy is updated, we will be able to remove the quotes and use the following at the top of the file instead: from __future__ import annotations
200 lines
6 KiB
Python
200 lines
6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
import pprint
|
|
import time
|
|
from typing import Any, Dict, Optional
|
|
|
|
import anki # pylint: disable=unused-import
|
|
from anki.consts import *
|
|
from anki.hooks import runHook
|
|
from anki.notes import Note
|
|
from anki.utils import intTime, joinFields, timestampID
|
|
|
|
# Cards
|
|
##########################################################################
|
|
|
|
# Type: 0=new, 1=learning, 2=due
|
|
# Queue: same as above, and:
|
|
# -1=suspended, -2=user buried, -3=sched buried
|
|
# Due is used differently for different queues.
|
|
# - new queue: note id or random int
|
|
# - rev queue: integer day
|
|
# - lrn queue: integer timestamp
|
|
|
|
class Card:
|
|
_qa: Optional[Dict[str,str]]
|
|
_note: Optional[Note]
|
|
timerStarted: Optional[float]
|
|
lastIvl: Optional[int]
|
|
|
|
def __init__(self, col: "anki.collection._Collection", id: Optional[int] = None) -> None:
|
|
self.col = col
|
|
self.timerStarted = None
|
|
self._qa = None
|
|
self._note = None
|
|
if id:
|
|
self.id = id
|
|
self.load()
|
|
else:
|
|
# to flush, set nid, ord, and due
|
|
self.id = timestampID(col.db, "cards")
|
|
self.did = 1
|
|
self.crt = intTime()
|
|
self.type = 0
|
|
self.queue = 0
|
|
self.ivl = 0
|
|
self.factor = 0
|
|
self.reps = 0
|
|
self.lapses = 0
|
|
self.left = 0
|
|
self.odue = 0
|
|
self.odid = 0
|
|
self.flags = 0
|
|
self.data = ""
|
|
|
|
def load(self) -> None:
|
|
(self.id,
|
|
self.nid,
|
|
self.did,
|
|
self.ord,
|
|
self.mod,
|
|
self.usn,
|
|
self.type,
|
|
self.queue,
|
|
self.due,
|
|
self.ivl,
|
|
self.factor,
|
|
self.reps,
|
|
self.lapses,
|
|
self.left,
|
|
self.odue,
|
|
self.odid,
|
|
self.flags,
|
|
self.data) = self.col.db.first(
|
|
"select * from cards where id = ?", self.id)
|
|
self._qa = None
|
|
self._note = None
|
|
|
|
def flush(self) -> None:
|
|
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):
|
|
runHook("odueInvalid")
|
|
assert self.due < 4294967296
|
|
self.col.db.execute(
|
|
"""
|
|
insert or replace into cards values
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
self.id,
|
|
self.nid,
|
|
self.did,
|
|
self.ord,
|
|
self.mod,
|
|
self.usn,
|
|
self.type,
|
|
self.queue,
|
|
self.due,
|
|
self.ivl,
|
|
self.factor,
|
|
self.reps,
|
|
self.lapses,
|
|
self.left,
|
|
self.odue,
|
|
self.odid,
|
|
self.flags,
|
|
self.data)
|
|
self.col.log(self)
|
|
|
|
def flushSched(self) -> None:
|
|
self.mod = intTime()
|
|
self.usn = self.col.usn()
|
|
# bug checks
|
|
if self.queue == 2 and self.odue and not self.col.decks.isDyn(self.did):
|
|
runHook("odueInvalid")
|
|
assert self.due < 4294967296
|
|
self.col.db.execute(
|
|
"""update cards set
|
|
mod=?, usn=?, type=?, queue=?, due=?, ivl=?, factor=?, reps=?,
|
|
lapses=?, left=?, odue=?, odid=?, did=? where id = ?""",
|
|
self.mod, self.usn, self.type, self.queue, self.due, self.ivl,
|
|
self.factor, self.reps, self.lapses,
|
|
self.left, self.odue, self.odid, self.did, self.id)
|
|
self.col.log(self)
|
|
|
|
def q(self, reload: bool = False, browser: bool = False) -> str:
|
|
return self.css() + self._getQA(reload, browser)['q']
|
|
|
|
def a(self) -> str:
|
|
return self.css() + self._getQA()['a']
|
|
|
|
def css(self) -> str:
|
|
return "<style>%s</style>" % self.model()['css']
|
|
|
|
def _getQA(self, reload: bool = False, browser: bool = False) -> Any:
|
|
if not self._qa or reload:
|
|
f = self.note(reload); m = self.model(); t = self.template()
|
|
data = [self.id, f.id, m['id'], self.odid or self.did, self.ord,
|
|
f.stringTags(), f.joinedFields(), self.flags]
|
|
if browser:
|
|
args = [t.get('bqfmt'), t.get('bafmt')]
|
|
else:
|
|
args = []
|
|
self._qa = self.col._renderQA(data, *args) # type: ignore
|
|
return self._qa
|
|
|
|
def note(self, reload: bool = False) -> Any:
|
|
if not self._note or reload:
|
|
self._note = self.col.getNote(self.nid)
|
|
return self._note
|
|
|
|
def model(self) -> Any:
|
|
return self.col.models.get(self.note().mid)
|
|
|
|
def template(self) -> Any:
|
|
m = self.model()
|
|
if m['type'] == MODEL_STD:
|
|
return self.model()['tmpls'][self.ord]
|
|
else:
|
|
return self.model()['tmpls'][0]
|
|
|
|
def startTimer(self) -> None:
|
|
self.timerStarted = time.time()
|
|
|
|
def timeLimit(self) -> Any:
|
|
"Time limit for answering in milliseconds."
|
|
conf = self.col.decks.confForDid(self.odid or self.did)
|
|
return conf['maxTaken']*1000
|
|
|
|
def shouldShowTimer(self) -> Any:
|
|
conf = self.col.decks.confForDid(self.odid or self.did)
|
|
return conf['timer']
|
|
|
|
def timeTaken(self) -> Any:
|
|
"Time taken to answer card, in integer MS."
|
|
total = int((time.time() - self.timerStarted)*1000)
|
|
return min(total, self.timeLimit())
|
|
|
|
def isEmpty(self) -> Optional[bool]:
|
|
ords = self.col.models.availOrds(
|
|
self.model(), joinFields(self.note().fields))
|
|
if self.ord not in ords:
|
|
return True
|
|
return False
|
|
|
|
def __repr__(self) -> str:
|
|
d = dict(self.__dict__)
|
|
# remove non-useful elements
|
|
del d['_note']
|
|
del d['_qa']
|
|
del d['col']
|
|
del d['timerStarted']
|
|
return pprint.pformat(d, width=300)
|
|
|
|
def userFlag(self) -> Any:
|
|
return self.flags & 0b111
|
|
|
|
def setUserFlag(self, flag: int) -> None:
|
|
assert 0 <= flag <= 7
|
|
self.flags = (self.flags & ~0b111) | flag
|