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

Previously we used getCard() to fetch a card at the time. This required a number of indices to perform efficiently, and the indices were expensive in terms of disk space and time required to keep them up to date. Instead we now gather a bunch of cards at once. - Drop checkDue()/isDue so writes are not necessary to the DB when checking for due cards - Due counts checked on deck load, and only updated once a day or at the end of a session. This prevents cards from expiring during reviews, leading to confusing undo behaviour and due counts that go up instead of down as you review. The default will be to only expire cards once a day, which represents a change from the way things were done previously. - Set deck var defaults on deck load/create instead of upgrade, which should fix upgrade issues - The scheduling code can now have bits and pieces switched out, which should make review early / cram etc easier to integrate - Cards with priority <= 0 now have their type incremented by three, so we can get access to schedulable cards with a single column. - rebuildQueue() -> reset() - refresh() -> refreshSession() - Views and many of the indices on the cards table are now obsolete and will be removed in the future. I won't remove them straight away, so as to not break backward compatibility. - Use bigger intervals between successive card templates, as the previous intervals were too small to represent in doubles in some circumstances Still to do: - review early - learn more - failing mature cards where delay1 > delay0
289 lines
9.6 KiB
Python
289 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
|
|
|
"""\
|
|
Cards
|
|
====================
|
|
"""
|
|
__docformat__ = 'restructuredtext'
|
|
|
|
import time, sys, math, random
|
|
from anki.db import *
|
|
from anki.models import CardModel, Model, FieldModel, formatQA
|
|
from anki.facts import Fact, factsTable, Field
|
|
from anki.utils import parseTags, findTag, stripHTML, genID, hexifyID
|
|
|
|
# Cards
|
|
##########################################################################
|
|
|
|
cardsTable = Table(
|
|
'cards', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
|
|
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
|
|
Column('created', Float, nullable=False, default=time.time),
|
|
Column('modified', Float, nullable=False, default=time.time),
|
|
Column('tags', UnicodeText, nullable=False, default=u""),
|
|
Column('ordinal', Integer, nullable=False),
|
|
# cached - changed on fact update
|
|
Column('question', UnicodeText, nullable=False, default=u""),
|
|
Column('answer', UnicodeText, nullable=False, default=u""),
|
|
# default to 'normal' priority;
|
|
# this is indexed in deck.py as we need to create a reverse index
|
|
Column('priority', Integer, nullable=False, default=2),
|
|
Column('interval', Float, nullable=False, default=0),
|
|
Column('lastInterval', Float, nullable=False, default=0),
|
|
Column('due', Float, nullable=False, default=time.time),
|
|
Column('lastDue', Float, nullable=False, default=0),
|
|
Column('factor', Float, nullable=False, default=2.5),
|
|
Column('lastFactor', Float, nullable=False, default=2.5),
|
|
Column('firstAnswered', Float, nullable=False, default=0),
|
|
# stats
|
|
Column('reps', Integer, nullable=False, default=0),
|
|
Column('successive', Integer, nullable=False, default=0),
|
|
Column('averageTime', Float, nullable=False, default=0),
|
|
Column('reviewTime', Float, nullable=False, default=0),
|
|
Column('youngEase0', Integer, nullable=False, default=0),
|
|
Column('youngEase1', Integer, nullable=False, default=0),
|
|
Column('youngEase2', Integer, nullable=False, default=0),
|
|
Column('youngEase3', Integer, nullable=False, default=0),
|
|
Column('youngEase4', Integer, nullable=False, default=0),
|
|
Column('matureEase0', Integer, nullable=False, default=0),
|
|
Column('matureEase1', Integer, nullable=False, default=0),
|
|
Column('matureEase2', Integer, nullable=False, default=0),
|
|
Column('matureEase3', Integer, nullable=False, default=0),
|
|
Column('matureEase4', Integer, nullable=False, default=0),
|
|
# this duplicates the above data, because there's no way to map imported
|
|
# data to the above
|
|
Column('yesCount', Integer, nullable=False, default=0),
|
|
Column('noCount', Integer, nullable=False, default=0),
|
|
# caching
|
|
Column('spaceUntil', Float, nullable=False, default=0),
|
|
Column('relativeDelay', Float, nullable=False, default=0), # obsolete
|
|
Column('isDue', Boolean, nullable=False, default=0), # obsolete
|
|
Column('type', Integer, nullable=False, default=2),
|
|
Column('combinedDue', Integer, nullable=False, default=0))
|
|
|
|
class Card(object):
|
|
"A card."
|
|
|
|
def __init__(self, fact=None, cardModel=None, created=None):
|
|
self.tags = u""
|
|
self.id = genID()
|
|
# new cards start as new & due
|
|
self.type = 2
|
|
self.isDue = True
|
|
self.timerStarted = False
|
|
self.timerStopped = False
|
|
self.modified = time.time()
|
|
if created:
|
|
self.created = created
|
|
self.due = created
|
|
else:
|
|
self.due = self.modified
|
|
self.combinedDue = self.due
|
|
if fact:
|
|
self.fact = fact
|
|
if cardModel:
|
|
self.cardModel = cardModel
|
|
# for non-orm use
|
|
self.cardModelId = cardModel.id
|
|
self.ordinal = cardModel.ordinal
|
|
d = {}
|
|
for f in self.fact.model.fieldModels:
|
|
d[f.name] = (f.id, self.fact[f.name])
|
|
qa = formatQA(None, fact.modelId, d, self.splitTags(), cardModel)
|
|
self.question = qa['question']
|
|
self.answer = qa['answer']
|
|
|
|
def setModified(self):
|
|
self.modified = time.time()
|
|
|
|
def startTimer(self):
|
|
self.timerStarted = time.time()
|
|
|
|
def stopTimer(self):
|
|
self.timerStopped = time.time()
|
|
|
|
def thinkingTime(self):
|
|
return (self.timerStopped or time.time()) - self.timerStarted
|
|
|
|
def totalTime(self):
|
|
return time.time() - self.timerStarted
|
|
|
|
def genFuzz(self):
|
|
"Generate a random offset to spread intervals."
|
|
self.fuzz = random.uniform(0.95, 1.05)
|
|
|
|
def htmlQuestion(self, type="question", align=True):
|
|
div = '''<div class="card%s" id="cm%s%s">%s</div>''' % (
|
|
type[0], type[0], hexifyID(self.cardModelId),
|
|
getattr(self, type))
|
|
# add outer div & alignment (with tables due to qt's html handling)
|
|
if not align:
|
|
return div
|
|
attr = type + 'Align'
|
|
if getattr(self.cardModel, attr) == 0:
|
|
align = "center"
|
|
elif getattr(self.cardModel, attr) == 1:
|
|
align = "left"
|
|
else:
|
|
align = "right"
|
|
return (("<center><table width=95%%><tr><td align=%s>" % align) +
|
|
div + "</td></tr></table></center>")
|
|
|
|
def htmlAnswer(self, align=True):
|
|
return self.htmlQuestion(type="answer", align=align)
|
|
|
|
def updateStats(self, ease, state):
|
|
self.reps += 1
|
|
if ease > 1:
|
|
self.successive += 1
|
|
else:
|
|
self.successive = 0
|
|
delay = self.totalTime()
|
|
# ignore any times over 60 seconds
|
|
if delay < 60:
|
|
self.reviewTime += delay
|
|
if self.averageTime:
|
|
self.averageTime = (self.averageTime + delay) / 2.0
|
|
else:
|
|
self.averageTime = delay
|
|
# we don't track first answer for cards
|
|
if state == "new":
|
|
state = "young"
|
|
# update ease and yes/no count
|
|
attr = state + "Ease%d" % ease
|
|
setattr(self, attr, getattr(self, attr) + 1)
|
|
if ease < 2:
|
|
self.noCount += 1
|
|
else:
|
|
self.yesCount += 1
|
|
if not self.firstAnswered:
|
|
self.firstAnswered = time.time()
|
|
self.setModified()
|
|
|
|
def splitTags(self):
|
|
return (self.fact.tags, self.fact.model.tags, self.cardModel.name)
|
|
|
|
def allTags(self):
|
|
"Non-canonified string of all tags."
|
|
return (self.fact.tags + "," +
|
|
self.fact.model.tags)
|
|
|
|
def hasTag(self, tag):
|
|
return findTag(tag, parseTags(self.allTags()))
|
|
|
|
def fromDB(self, s, id):
|
|
r = s.first("""select
|
|
id, factId, cardModelId, created, modified, tags, ordinal, question, answer,
|
|
priority, interval, lastInterval, due, lastDue, factor,
|
|
lastFactor, firstAnswered, reps, successive, averageTime, reviewTime,
|
|
youngEase0, youngEase1, youngEase2, youngEase3, youngEase4,
|
|
matureEase0, matureEase1, matureEase2, matureEase3, matureEase4,
|
|
yesCount, noCount, spaceUntil, isDue, type, combinedDue
|
|
from cards where id = :id""", id=id)
|
|
if not r:
|
|
return
|
|
(self.id,
|
|
self.factId,
|
|
self.cardModelId,
|
|
self.created,
|
|
self.modified,
|
|
self.tags,
|
|
self.ordinal,
|
|
self.question,
|
|
self.answer,
|
|
self.priority,
|
|
self.interval,
|
|
self.lastInterval,
|
|
self.due,
|
|
self.lastDue,
|
|
self.factor,
|
|
self.lastFactor,
|
|
self.firstAnswered,
|
|
self.reps,
|
|
self.successive,
|
|
self.averageTime,
|
|
self.reviewTime,
|
|
self.youngEase0,
|
|
self.youngEase1,
|
|
self.youngEase2,
|
|
self.youngEase3,
|
|
self.youngEase4,
|
|
self.matureEase0,
|
|
self.matureEase1,
|
|
self.matureEase2,
|
|
self.matureEase3,
|
|
self.matureEase4,
|
|
self.yesCount,
|
|
self.noCount,
|
|
self.spaceUntil,
|
|
self.isDue,
|
|
self.type,
|
|
self.combinedDue) = r
|
|
return True
|
|
|
|
def toDB(self, s):
|
|
"Write card to DB. Note that isDue assumes card is not spaced."
|
|
if self.reps == 0:
|
|
self.type = 2
|
|
elif self.successive:
|
|
self.type = 1
|
|
else:
|
|
self.type = 0
|
|
s.execute("""update cards set
|
|
modified=:modified,
|
|
tags=:tags,
|
|
interval=:interval,
|
|
lastInterval=:lastInterval,
|
|
due=:due,
|
|
lastDue=:lastDue,
|
|
factor=:factor,
|
|
lastFactor=:lastFactor,
|
|
firstAnswered=:firstAnswered,
|
|
reps=:reps,
|
|
successive=:successive,
|
|
averageTime=:averageTime,
|
|
reviewTime=:reviewTime,
|
|
youngEase0=:youngEase0,
|
|
youngEase1=:youngEase1,
|
|
youngEase2=:youngEase2,
|
|
youngEase3=:youngEase3,
|
|
youngEase4=:youngEase4,
|
|
matureEase0=:matureEase0,
|
|
matureEase1=:matureEase1,
|
|
matureEase2=:matureEase2,
|
|
matureEase3=:matureEase3,
|
|
matureEase4=:matureEase4,
|
|
yesCount=:yesCount,
|
|
noCount=:noCount,
|
|
spaceUntil = :spaceUntil,
|
|
isDue = :isDue,
|
|
type = :type,
|
|
combinedDue = max(:spaceUntil, :due),
|
|
relativeDelay = 0,
|
|
priority = :priority
|
|
where id=:id""", self.__dict__)
|
|
|
|
mapper(Card, cardsTable, properties={
|
|
'cardModel': relation(CardModel),
|
|
'fact': relation(Fact, backref="cards", primaryjoin=
|
|
cardsTable.c.factId == factsTable.c.id),
|
|
})
|
|
|
|
mapper(Fact, factsTable, properties={
|
|
'model': relation(Model),
|
|
'fields': relation(Field, backref="fact", order_by=Field.ordinal),
|
|
})
|
|
|
|
|
|
# Card deletions
|
|
##########################################################################
|
|
|
|
cardsDeletedTable = Table(
|
|
'cardsDeleted', metadata,
|
|
Column('cardId', Integer, ForeignKey("cards.id"),
|
|
nullable=False),
|
|
Column('deletedTime', Float, nullable=False))
|