Anki/anki/cards.py
Damien Elmes 55f4b9b7d0 favour integers, change due representation, fact&card ordering, more
- removed 'created' column from various tables. We don't care when things like
  models are created, and card creation time didn't reflect the actual time a
  card was created
- facts were previously ordered by their creation date. The code would
  manually set the creation time for subsequent facts on import by 0.0001
  seconds, and then card due times were set by adding the fact time to the
  ordinal number*0.000001. This was prone to error, and the number of zeros used
  was actually different in different parts of the code. Instead of this, we
  replace it with a 'pos' column on facts, which increments for each new fact.
- importing should add new facts with a higher pos, but concurrent updates in
  a synced deck can have multiple facts with the same pos

- due times are completely different now, and depend on the card type
- new cards have due=fact.pos or random(0, 10000)
- reviews have due set to an integer representing days since deck
  creation/download
- cards in the learn queue use an integer timestamp in seconds

- many columns like modified, lastSync, factor, interval, etc have been converted to
  integer columns. They are cheaper to store (large decks can save 10s of
  megabytes) and faster to search for.

- cards have their group assigned on fact creation. In the future we'll add a
  per-template option for a default group.

- switch to due/random order for the review queue on upgrade. Users can still
  switch to the old behaviour if they want, but many people don't care what
  it's set to, and due is considerably faster, which may result in a better
  user experience
2011-04-28 09:23:28 +09:00

199 lines
6.4 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
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, intTime
from anki.media import updateMediaCount, mediaFiles
MAX_TIMER = 60
# Cards
##########################################################################
# Type: 0=learning, 1=due, 2=new
# Queue: 0=learning, 1=due, 2=new
# -1=suspended, -2=user buried, -3=sched buried
# Group: scheduling group
# Ordinal: card template # for fact
# Flags: unused; reserved for future use
# Due is used differently for different queues.
# - new queue: fact.pos
# - rev queue: integer day
# - lrn queue: integer timestamp
cardsTable = Table(
'cards', metadata,
Column('id', Integer, primary_key=True),
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
Column('groupId', Integer, nullable=False, default=1),
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
Column('modified', Integer, nullable=False, default=intTime),
# general
Column('question', UnicodeText, nullable=False, default=u""),
Column('answer', UnicodeText, nullable=False, default=u""),
Column('ordinal', Integer, nullable=False),
Column('flags', Integer, nullable=False, default=0),
# shared scheduling
Column('type', Integer, nullable=False, default=2),
Column('queue', Integer, nullable=False, default=2),
Column('due', Integer, nullable=False),
# sm2
Column('interval', Integer, nullable=False, default=0),
Column('factor', Integer, nullable=False),
Column('reps', Integer, nullable=False, default=0),
Column('streak', Integer, nullable=False, default=0),
Column('lapses', Integer, nullable=False, default=0),
# learn
Column('grade', Integer, nullable=False, default=0),
Column('cycles', Integer, nullable=False, default=0)
)
class Card(object):
# called one of three ways:
# - with no args, followed by .fromDB()
# - with all args, when adding cards to db
def __init__(self, fact=None, cardModel=None, group=None):
# timer
self.timerStarted = None
if fact:
self.id = genID()
self.modified = intTime()
self.due = fact.pos
self.fact = fact
self.modelId = fact.modelId
self.cardModel = cardModel
self.groupId = group.id
self.factor = group.config['initialFactor']
# for non-orm use
self.cardModelId = cardModel.id
self.ordinal = cardModel.ordinal
def setModified(self):
self.modified = intTime()
def startTimer(self):
self.timerStarted = time.time()
def userTime(self):
return min(time.time() - self.timerStarted, MAX_TIMER)
# Questions and answers
##########################################################################
def rebuildQA(self, deck, media=True):
# format qa
d = {}
for f in self.fact.model.fieldModels:
d[f.name] = (f.id, self.fact[f.name])
qa = formatQA(None, self.fact.modelId, d, self._splitTags(),
self.cardModel, deck)
# find old media references
files = {}
for type in ("question", "answer"):
for f in mediaFiles(getattr(self, type) or ""):
if f in files:
files[f] -= 1
else:
files[f] = -1
# update q/a
self.question = qa['question']
self.answer = qa['answer']
# determine media delta
for type in ("question", "answer"):
for f in mediaFiles(getattr(self, type)):
if f in files:
files[f] += 1
else:
files[f] = 1
# update media counts if we're attached to deck
if media:
for (f, cnt) in files.items():
updateMediaCount(deck, f, cnt)
self.setModified()
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 _splitTags(self):
return (self.fact.tags, self.fact.model.name, self.cardModel.name)
# Non-ORM
##########################################################################
def fromDB(self, s, id):
r = s.first("""select * from cards where id = :id""", id=id)
if not r:
return
(self.id,
self.factId,
self.groupId,
self.cardModelId,
self.modified,
self.question,
self.answer,
self.ordinal,
self.flags,
self.type,
self.queue,
self.due,
self.interval,
self.factor,
self.reps,
self.streak,
self.lapses,
self.grade,
self.cycles) = r
return True
def toDB(self, s):
# this shouldn't be used for schema changes
s.execute("""update cards set
modified=:modified,
question=:question,
answer=:answer,
flags=:flags,
type=:type,
queue=:queue,
due=:due,
interval=:interval,
factor=:factor,
reps=:reps,
streak=:streak,
lapses=:lapses,
grade=:grade,
cycles=:cycles
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),
})