# -*- coding: utf-8 -*- # Copyright: Damien Elmes # 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 from anki.facts import Fact, factsTable, Field from anki.utils import parseTags, findTag, stripHTML, genID # 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), # cache Column('spaceUntil', Float, nullable=False, default=0), Column('relativeDelay', Float, nullable=False, default=0), Column('isDue', Boolean, nullable=False, default=0), 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): self.tags = u"" self.id = genID() # new cards start as new & due self.type = 2 self.isDue = True self.timerStarted = False self.timerStopped = False if fact: self.fact = fact if cardModel: self.cardModel = cardModel self.ordinal = cardModel.ordinal self.question = cardModel.renderQA(self, self.fact, "question") self.answer = cardModel.renderQA(self, self.fact, "answer") htmlQuestion = property(lambda self: self.cardModel.renderQA( self, self.fact, "question", format="html")) htmlAnswer = property(lambda self: self.cardModel.renderQA( self, self.fact, "answer", format="html")) 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 the time this card's been shown." return (self.timerStopped or time.time()) - self.timerStarted def css(self): return self.cardModel.css() + self.fact.css() def genFuzz(self): "Generate a random offset to spread intervals." self.fuzz = random.uniform(0.95, 1.05) def updateStats(self, ease, state): self.reps += 1 if ease > 1: self.successive += 1 else: self.successive = 0 delay = self.thinkingTime() # 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 hasTag(self, tag): alltags = parseTags(self.tags + "," + self.fact.tags + "," + self.cardModel.name + "," + self.fact.model.tags) return findTag(tag, alltags) def fromDB(self, s, id): r = s.first("select * from cards where id = :id", id=id) (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.relativeDelay, self.isDue, self.type, self.combinedDue) = r 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, relativeDelay = :interval / (strftime("%s", "now") - :due + 1), isDue = :isDue, type = :type, combinedDue = max(:spaceUntil, :due) 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.c.ordinal), 'lastCard': relation(Card, post_update=True, primaryjoin= cardsTable.c.id == factsTable.c.lastCardId), }) # Card deletions ########################################################################## cardsDeletedTable = Table( 'cardsDeleted', metadata, Column('cardId', Integer, ForeignKey("cards.id"), nullable=False), Column('deletedTime', Float, nullable=False))