start work on learn mode, change models, more

- model config is now stored as a json-serialized dict, which allows us to
  quickly gather the info and allows for adding extra options more easily in
  the future
- denormalize modelId into the cards table, so we can get the model scheduling
  information without having to hit the facts table
- remove position - since we will handle spacing differently we don't need a
  separate variable to due to define sort order
- remove lastInterval from cards; the new cram mode and review early shouldn't
  need it
- successive->streak
- add new columns for learn mode
- move cram mode into new file; learn more and review early need more thought
- initial work on learn mode
- initial unit tests
This commit is contained in:
Damien Elmes 2011-02-28 14:48:52 +09:00
parent 4e7e8b03bc
commit b0b4074cbd
9 changed files with 479 additions and 381 deletions

View file

@ -14,46 +14,39 @@ MAX_TIMER = 60
# Cards
##########################################################################
# tasks:
# - remove all failed cards from learning queue - set queue=1; type=1 and
# leave scheduling parameters alone (need separate due for learn queue and
# reviews)
#
# - cram cards. gather and introduce to queue=0.
# - remove all cram cards from learning queue. if type h
# Type: 0=new+learning, 1=due, 2=new, 3=failed+learning, 4=cram+learning
# Queue: 0=learning, 1=due, 2=new, 3=new today,
# -1=suspended, -2=user buried, -3=sched buried (rev early, etc)
# Type: 0=learning, 1=due, 2=new
# Queue: 0=learning, 1=due, 2=new
# -1=suspended, -2=user buried, -3=sched buried
# Ordinal: card template # for fact
# Position: sorting position, only for new cards
# Flags: unused; reserved for future use
cardsTable = Table(
'cards', metadata,
Column('id', Integer, primary_key=True),
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
Column('modelId', Integer, ForeignKey("models.id"), nullable=False),
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
# general
Column('created', Float, nullable=False, default=time.time),
Column('modified', Float, nullable=False, default=time.time),
Column('question', UnicodeText, nullable=False, default=u""),
Column('answer', UnicodeText, nullable=False, default=u""),
Column('flags', Integer, nullable=False, default=0),
# ordering
Column('ordinal', Integer, nullable=False),
Column('position', Integer, nullable=False),
# scheduling data
Column('flags', Integer, nullable=False, default=0),
# shared scheduling
Column('type', Integer, nullable=False, default=2),
Column('queue', Integer, nullable=False, default=2),
Column('lastInterval', Float, nullable=False, default=0),
Column('interval', Float, nullable=False, default=0),
Column('due', Float, nullable=False),
# sm2
Column('interval', Float, nullable=False, default=0),
Column('factor', Float, nullable=False, default=2.5),
# counters
Column('reps', Integer, nullable=False, default=0),
Column('successive', Integer, nullable=False, default=0),
Column('lapses', 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):
@ -68,6 +61,7 @@ class Card(object):
self.position = self.due
if fact:
self.fact = fact
self.modelId = fact.modelId
if cardModel:
self.cardModel = cardModel
# for non-orm use
@ -151,45 +145,47 @@ class Card(object):
return
(self.id,
self.factId,
self.modelId,
self.cardModelId,
self.created,
self.modified,
self.question,
self.answer,
self.flags,
self.ordinal,
self.position,
self.flags,
self.type,
self.queue,
self.lastInterval,
self.interval,
self.due,
self.interval,
self.factor,
self.reps,
self.successive,
self.lapses) = r
self.streak,
self.lapses,
self.grade,
self.cycles) = r
return True
def toDB(self, s):
s.execute("""update cards set
factId=:factId,
modelId=:modelId,
cardModelId=:cardModelId,
created=:created,
modified=:modified,
question=:question,
answer=:answer,
flags=:flags,
ordinal=:ordinal,
position=:position,
flags=:flags,
type=:type,
queue=:queue,
lastInterval=:lastInterval,
interval=:interval,
due=:due,
interval=:interval,
factor=:factor,
reps=:reps,
successive=:successive,
lapses=:lapses
streak=:streak,
lapses=:lapses,
grade=:grade,
cycles=:cycles
where id=:id""", self.__dict__)
mapper(Card, cardsTable, properties={

118
anki/cram.py Normal file
View file

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
from anki.sched import Scheduler
class CramScheduler(Scheduler):
# Cramming
##########################################################################
def setupCramScheduler(self, active, order):
self.getCardId = self._getCramCardId
self.activeCramTags = active
self.cramOrder = order
self.rebuildNewCount = self._rebuildCramNewCount
self.rebuildRevCount = self._rebuildCramCount
self.rebuildLrnCount = self._rebuildLrnCramCount
self.fillRevQueue = self._fillCramQueue
self.fillLrnQueue = self._fillLrnCramQueue
self.finishScheduler = self.setupStandardScheduler
self.lrnCramQueue = []
print "requeue cram"
self.requeueCard = self._requeueCramCard
self.cardQueue = self._cramCardQueue
self.answerCard = self._answerCramCard
self.spaceCards = self._spaceCramCards
# reuse review early's code
self.answerPreSave = self._cramPreSave
self.cardLimit = self._cramCardLimit
self.scheduler = "cram"
def _cramPreSave(self, card, ease):
# prevent it from appearing in next queue fill
card.lastInterval = self.cramLastInterval
card.type = -3
def _spaceCramCards(self, card):
self.spacedFacts[card.factId] = time.time() + self.newSpacing
def _answerCramCard(self, card, ease):
self.cramLastInterval = card.lastInterval
self._answerCard(card, ease)
if ease == 1:
self.lrnCramQueue.insert(0, [card.id, card.factId])
def _getCramCardId(self, check=True):
self.checkDay()
self.fillQueues()
if self.lrnCardMax and self.learnCount >= self.lrnCardMax:
return self.lrnQueue[-1][0]
# card due for review?
if self.revNoSpaced():
return self.revQueue[-1][0]
if self.lrnQueue:
return self.lrnQueue[-1][0]
if check:
# collapse spaced cards before reverting back to old scheduler
self.reset()
return self.getCardId(False)
# if we're in a custom scheduler, we may need to switch back
if self.finishScheduler:
self.finishScheduler()
self.reset()
return self.getCardId()
def _cramCardQueue(self, card):
if self.revQueue and self.revQueue[-1][0] == card.id:
return 1
else:
return 0
def _requeueCramCard(self, card, oldSuc):
if self.cardQueue(card) == 1:
self.revQueue.pop()
else:
self.lrnCramQueue.pop()
def _rebuildCramNewCount(self):
self.newAvail = 0
self.newCount = 0
def _cramCardLimit(self, active, inactive, sql):
# inactive is (currently) ignored
if isinstance(active, list):
return sql.replace(
"where", "where +c.id in " + ids2str(active) + " and")
else:
yes = parseTags(active)
if yes:
yids = tagIds(self.db, yes).values()
return sql.replace(
"where ",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and " % ids2str(yids))
else:
return sql
def _fillCramQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.db.all(self.cardLimit(
self.activeCramTags, "", """
select id, factId from cards c
where queue between 0 and 2
order by %s
limit %s""" % (self.cramOrder, self.queueLimit)))
self.revQueue.reverse()
def _rebuildCramCount(self):
self.revCount = self.db.scalar(self.cardLimit(
self.activeCramTags, "",
"select count(*) from cards c where queue between 0 and 2"))
def _rebuildLrnCramCount(self):
self.learnCount = len(self.lrnCramQueue)
def _fillLrnCramQueue(self):
self.lrnQueue = self.lrnCramQueue

View file

@ -133,6 +133,9 @@ class Deck(object):
def getCard(self):
return self.sched.getCard()
def answerCard(self, card, ease):
self.sched.answerCard(card, ease)
# if card:
# return card
# if sched.name == "main":

View file

@ -2,7 +2,7 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import time, re
import time, re, simplejson
from sqlalchemy.ext.orderinglist import ordering_list
from anki.db import *
from anki.utils import genID, canonifyTags
@ -153,9 +153,6 @@ def formatQA(cid, mid, fact, tags, cm, deck):
# Model table
##########################################################################
# ...Schedule: fail, pass1, pass2, etc in minutes
# ...Intervals: graduation, first remove, later remove
# maybe define a random cutoff at say +/-30% which controls exit interval
# variation - 30% of 1 day is 0.7 or 1.3 so always 1 day; 30% of 4 days is
# 2.8-5.2, so any time from 3-5 days is acceptable
@ -166,27 +163,29 @@ def formatQA(cid, mid, fact, tags, cm, deck):
# optional, what intervals should the default be? 3 days or more if cards are
# over that interval range? and what about failed mature bonus?
defaultConf = {
'new': {
'delays': [0.5, 3, 10],
'ints': [1, 7, 4],
},
'lapse': {
'delays': [0.5, 3, 10],
'ints': [1, 7, 4],
'mult': 0
},
'initialFactor': 2.5,
}
modelsTable = Table(
'models', metadata,
Column('id', Integer, primary_key=True),
Column('created', Float, nullable=False, default=time.time),
Column('modified', Float, nullable=False, default=time.time),
Column('name', UnicodeText, nullable=False),
# new cards
Column('newSched', UnicodeText, nullable=False, default=u"[0.5, 3, 10]"),
Column('newInts', UnicodeText, nullable=False, default=u"[1, 7, 4]"),
# failed cards
Column('failSched', UnicodeText, nullable=False, default=u"[0.5, 3, 10]"),
Column('failInts', UnicodeText, nullable=False, default=u"[1, 7, 4]"),
Column('failMult', Float, nullable=False, default=0),
# other scheduling
Column('initialFactor', Float, nullable=False, default=2.5),
Column('config', UnicodeText, nullable=False,
default=unicode(simplejson.dumps(defaultConf))),
)
class Model(object):
"Defines the way a fact behaves, what fields it can contain, etc."
def __init__(self, name=u""):

View file

@ -2,11 +2,12 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import time, datetime
import time, datetime, simplejson
from heapq import *
from anki.db import *
from anki.cards import Card
from anki.utils import parseTags
from anki.lang import _
# the standard Anki scheduler
class Scheduler(object):
@ -26,106 +27,57 @@ class Scheduler(object):
def getCard(self, orm=True):
"Pop the next card from the queue. None if finished."
id = self._getCard()
self.checkDay()
id = self.getCardId()
if id:
card = Card()
assert card.fromDB(self.db, id)
return card
def reset(self):
self.modelConfigs = {}
self.resetLearn()
self.resetReview()
self.resetNew()
print "reset(); need to handle new cards"
# # day counts
# (self.repsToday, self.newSeenToday) = self.db.first("""
# select count(), sum(case when rep = 1 then 1 else 0 end) from revlog
# where time > :t""", t=self.dayCutoff-86400)
# self.newSeenToday = self.newSeenToday or 0
# print "newSeenToday in answer(), reset called twice"
# print "newSeenToday needs to account for drill mode too."
# FIXME: can we do this now with the learn queue?
def rebuildTypes(self):
"Rebuild the type cache. Only necessary on upgrade."
# set type first
self.db.statement("""
update cards set type = (case
when successive then 1 when reps then 0 else 2 end)
""")
# then queue
self.db.statement("""
update cards set queue = type
when queue != -1""")
def answerCard(self, card, ease):
if card.queue == 0:
self.answerLearnCard(card, ease)
elif card.queue == 1:
self.answerRevCard(card, ease)
else:
raise Exception("Invalid queue")
card.toDB(self.db)
def counts(self):
# FIXME: should learn count include new cards due today, or be separate?
return (self.learnCount, self.revCount)
# FIXME: merge these into the fetching code? rely on the type/queue
# properties? have to think about implications for cramming
def cardQueue(self, card):
return self.cardType(card)
return card.queue
def cardType(self, card):
"Return the type of the current card (what queue it's in)"
if card.successive:
return 1
elif card.reps:
return 0
else:
return 2
# Tools
# Getting the next card
##########################################################################
def resetSchedBuried(self):
"Put temporarily suspended cards back into play."
self.db.statement(
"update cards set queue = type where queue = -3")
def cardLimit(self, active, inactive, sql):
yes = parseTags(getattr(self.deck, active))
no = parseTags(getattr(self.deck, inactive))
if yes:
yids = tagIds(self.db, yes).values()
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and +c.id not in (select cardId from "
"cardTags where tagId in %s) and" % (
ids2str(yids),
ids2str(nids)))
elif no:
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id not in (select cardId from cardTags where "
"tagId in %s) and" % ids2str(nids))
else:
return sql
# Daily cutoff
##########################################################################
def updateCutoff(self):
d = datetime.datetime.utcfromtimestamp(
time.time() - self.deck.utcOffset) + datetime.timedelta(days=1)
d = datetime.datetime(d.year, d.month, d.day)
newday = self.deck.utcOffset - time.timezone
d += datetime.timedelta(seconds=newday)
cutoff = time.mktime(d.timetuple())
# cutoff must not be in the past
while cutoff < time.time():
cutoff += 86400
# cutoff must not be more than 24 hours in the future
cutoff = min(time.time() + 86400, cutoff)
self.dayCutoff = cutoff
self.dayCount = int(cutoff/86400 - self.deck.created/86400)
print "dayCount", self.dayCount
def checkDay(self):
# check if the day has rolled over
if time.time() > self.dayCutoff:
self.updateCutoff()
self.reset()
def getCardId(self):
"Return the next due card id, or None."
# learning card due?
id = self.getLearnCard()
if id:
return id
# new first, or time for one?
if self.timeForNewCard():
return self.getNewCard()
# card due for review?
id = self.getReviewCard()
if id:
return id
# new cards left?
id = self.getNewCard()
if id:
return id
# collapse or finish
return self.getLearnCard(collapse=True)
# Learning queue
##########################################################################
@ -135,12 +87,64 @@ when queue != -1""")
select due, id from cards where
queue = 0 and due < :lim order by due
limit %d""" % self.learnLimit, lim=self.dayCutoff)
self.learnQueue.reverse()
self.learnCount = len(self.learnQueue)
def getLearnCard(self):
if self.learnQueue and self.learnQueue[0] < time.time():
return heappop(self.learnQueue)
def getLearnCard(self, collapse=False):
if self.learnQueue:
cutoff = time.time()
if collapse:
cutoff -= self.deck.collapseTime
if self.learnQueue[0][0] < cutoff:
return heappop(self.learnQueue)[1]
def answerLearnCard(self, card, ease):
# ease 1=no, 2=yes, 3=remove
conf = self.learnConf(card)
if ease == 3:
self.removeLearnCard(card, conf)
return
card.cycles += 1
if ease == 2:
card.grade += 1
else:
card.grade = 0
if card.grade >= len(conf['delays']):
self.graduateLearnCard(card, conf)
else:
card.due = time.time() + conf['delays'][card.grade]*60
def learnConf(self, card):
conf = self.configForCard(card)
if card.type == 2:
return conf['new']
else:
return conf['lapse']
def removeLearnCard(self, card, conf):
if card.type == 1:
int_ = None
elif not card.cycles:
# first time bonus
int_ = conf['ints'][1]
else:
# normal remove
int_ = conf['ints'][2]
self.rescheduleAsReview(card, int_)
def graduateLearnCard(self, card, conf):
if card.type == 1:
int_ = None
else:
int_ = conf['ints'][0]
self.rescheduleAsReview(card, int_)
def rescheduleAsReview(self, card, int_):
card.queue = 1
if int_:
# new card
card.type = 1
card.interval = int_
print "handle log, etc"
# Reviews
##########################################################################
@ -183,128 +187,10 @@ limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dayCutoff)
def showFailedLast(self):
return self.collapseTime or not self.delay0
# New cards
##########################################################################
# when do we do this?
#self.updateNewCountToday()
def resetNew(self):
# self.updateNewCardRatio()
pass
def rebuildNewCount(self):
self.newAvail = self.db.scalar(
self.cardLimit(
"newActive", "newInactive",
"select count(*) from cards c where queue = 2 "
"and due < :lim"), lim=self.dayCutoff)
self.updateNewCountToday()
self.spacedCards = []
def updateNewCountToday(self):
self.newCount = max(min(
self.newAvail, self.newCardsPerDay -
self.newSeenToday), 0)
def fillNewQueue(self):
if self.newCount and not self.newQueue and not self.spacedCards:
self.newQueue = self.db.all(
self.cardLimit(
"newActive", "newInactive", """
select c.id, factId from cards c where
queue = 2 and due < :lim order by %s
limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dayCutoff)
self.newQueue.reverse()
def updateNewCardRatio(self):
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
if self.newCount:
self.newCardModulus = (
(self.newCount + self.revCount) / self.newCount)
# if there are cards to review, ensure modulo >= 2
if self.revCount:
self.newCardModulus = max(2, self.newCardModulus)
else:
self.newCardModulus = 0
else:
self.newCardModulus = 0
def timeForNewCard(self):
"True if it's time to display a new card when distributing."
if not self.newCount:
return False
if self.newCardSpacing == NEW_CARDS_LAST:
return False
if self.newCardSpacing == NEW_CARDS_FIRST:
return True
if self.newCardModulus:
return self.repsToday % self.newCardModulus == 0
else:
return False
def getNewCard(self):
src = None
if (self.spacedCards and
self.spacedCards[0][0] < time.time()):
# spaced card has expired
src = 0
elif self.newQueue:
# card left in new queue
src = 1
elif self.spacedCards:
# card left in spaced queue
src = 0
else:
# only cards spaced to another day left
return
if src == 0:
cards = self.spacedCards[0][1]
self.newFromCache = True
return cards[0]
else:
self.newFromCache = False
return self.newQueue[-1][0]
def newOrder(self):
return ("due",
"due",
"due desc")[self.newCardOrder]
# Getting the next card
##########################################################################
def getCard(self):
"Return the next due card id, or None."
self.checkDay()
# learning card due?
id = self.getLearnCard()
if id:
return id
# distribute new cards?
if self.newNoSpaced() and self.timeForNewCard():
return self.getNewCard()
# card due for review?
if self.revNoSpaced():
return self.revQueue[-1][0]
# new cards left?
if self.newCount:
id = self.getNewCard()
if id:
return id
# display failed cards early/last
if not check and self.showFailedLast() and self.learnQueue:
return self.learnQueue[-1][0]
# if we're in a custom scheduler, we may need to switch back
if self.finishScheduler:
self.finishScheduler()
self.reset()
return self.getCardId()
# Answering a card
##########################################################################
def answerCard(self, card, ease):
def _answerCard(self, card, ease):
undoName = _("Answer Card")
self.setUndoStart(undoName)
now = time.time()
@ -520,6 +406,148 @@ and queue between 1 and 2""",
self.reset()
self.refreshSession()
# New cards
##########################################################################
# # day counts
# (self.repsToday, self.newSeenToday) = self.db.first("""
# select count(), sum(case when rep = 1 then 1 else 0 end) from revlog
# where time > :t""", t=self.dayCutoff-86400)
# self.newSeenToday = self.newSeenToday or 0
# print "newSeenToday in answer(), reset called twice"
# print "newSeenToday needs to account for drill mode too."
# when do we do this?
#self.updateNewCountToday()
def resetNew(self):
# self.updateNewCardRatio()
pass
def rebuildNewCount(self):
self.newAvail = self.db.scalar(
self.cardLimit(
"newActive", "newInactive",
"select count(*) from cards c where queue = 2 "
"and due < :lim"), lim=self.dayCutoff)
self.updateNewCountToday()
def updateNewCountToday(self):
self.newCount = max(min(
self.newAvail, self.newCardsPerDay -
self.newSeenToday), 0)
def fillNewQueue(self):
if self.newCount and not self.newQueue:
self.newQueue = self.db.all(
self.cardLimit(
"newActive", "newInactive", """
select c.id, factId from cards c where
queue = 2 and due < :lim order by %s
limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dayCutoff)
self.newQueue.reverse()
def updateNewCardRatio(self):
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
if self.newCount:
self.newCardModulus = (
(self.newCount + self.revCount) / self.newCount)
# if there are cards to review, ensure modulo >= 2
if self.revCount:
self.newCardModulus = max(2, self.newCardModulus)
else:
self.newCardModulus = 0
else:
self.newCardModulus = 0
def timeForNewCard(self):
"True if it's time to display a new card when distributing."
# FIXME
return False
if not self.newCount:
return False
if self.newCardSpacing == NEW_CARDS_LAST:
return False
if self.newCardSpacing == NEW_CARDS_FIRST:
return True
if self.newCardModulus:
return self.repsToday % self.newCardModulus == 0
else:
return False
def getNewCard(self):
# FIXME
return None
#return self.newQueue[-1][0]
def newOrder(self):
return ("due",
"due",
"due desc")[self.newCardOrder]
# Tools
##########################################################################
def configForCard(self, card):
mid = card.modelId
if not mid in self.modelConfigs:
self.modelConfigs[mid] = simplejson.loads(
self.db.scalar("select config from models where id = :id",
id=mid))
return self.modelConfigs[mid]
def resetSchedBuried(self):
"Put temporarily suspended cards back into play."
self.db.statement(
"update cards set queue = type where queue = -3")
def cardLimit(self, active, inactive, sql):
yes = parseTags(getattr(self.deck, active))
no = parseTags(getattr(self.deck, inactive))
if yes:
yids = tagIds(self.db, yes).values()
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and +c.id not in (select cardId from "
"cardTags where tagId in %s) and" % (
ids2str(yids),
ids2str(nids)))
elif no:
nids = tagIds(self.db, no).values()
return sql.replace(
"where",
"where +c.id not in (select cardId from cardTags where "
"tagId in %s) and" % ids2str(nids))
else:
return sql
# Daily cutoff
##########################################################################
def updateCutoff(self):
d = datetime.datetime.utcfromtimestamp(
time.time() - self.deck.utcOffset) + datetime.timedelta(days=1)
d = datetime.datetime(d.year, d.month, d.day)
newday = self.deck.utcOffset - time.timezone
d += datetime.timedelta(seconds=newday)
cutoff = time.mktime(d.timetuple())
# cutoff must not be in the past
while cutoff < time.time():
cutoff += 86400
# cutoff must not be more than 24 hours in the future
cutoff = min(time.time() + 86400, cutoff)
self.dayCutoff = cutoff
self.dayCount = int(cutoff/86400 - self.deck.created/86400)
def checkDay(self):
# check if the day has rolled over
if time.time() > self.dayCutoff:
self.updateCutoff()
self.reset()
# Review early
##########################################################################
@ -571,115 +599,3 @@ order by due limit %d""" % self.queueLimit), lim=self.dayCutoff)
def _updateLearnMoreCountToday(self):
self.newCount = self.newAvail
# Cramming
##########################################################################
def setupCramScheduler(self, active, order):
self.getCardId = self._getCramCardId
self.activeCramTags = active
self.cramOrder = order
self.rebuildNewCount = self._rebuildCramNewCount
self.rebuildRevCount = self._rebuildCramCount
self.rebuildLrnCount = self._rebuildLrnCramCount
self.fillRevQueue = self._fillCramQueue
self.fillLrnQueue = self._fillLrnCramQueue
self.finishScheduler = self.setupStandardScheduler
self.lrnCramQueue = []
print "requeue cram"
self.requeueCard = self._requeueCramCard
self.cardQueue = self._cramCardQueue
self.answerCard = self._answerCramCard
self.spaceCards = self._spaceCramCards
# reuse review early's code
self.answerPreSave = self._cramPreSave
self.cardLimit = self._cramCardLimit
self.scheduler = "cram"
def _cramPreSave(self, card, ease):
# prevent it from appearing in next queue fill
card.lastInterval = self.cramLastInterval
card.type = -3
def _spaceCramCards(self, card):
self.spacedFacts[card.factId] = time.time() + self.newSpacing
def _answerCramCard(self, card, ease):
self.cramLastInterval = card.lastInterval
self._answerCard(card, ease)
if ease == 1:
self.lrnCramQueue.insert(0, [card.id, card.factId])
def _getCramCardId(self, check=True):
self.checkDay()
self.fillQueues()
if self.lrnCardMax and self.lrnCount >= self.lrnCardMax:
return self.lrnQueue[-1][0]
# card due for review?
if self.revNoSpaced():
return self.revQueue[-1][0]
if self.lrnQueue:
return self.lrnQueue[-1][0]
if check:
# collapse spaced cards before reverting back to old scheduler
self.reset()
return self.getCardId(False)
# if we're in a custom scheduler, we may need to switch back
if self.finishScheduler:
self.finishScheduler()
self.reset()
return self.getCardId()
def _cramCardQueue(self, card):
if self.revQueue and self.revQueue[-1][0] == card.id:
return 1
else:
return 0
def _requeueCramCard(self, card, oldSuc):
if self.cardQueue(card) == 1:
self.revQueue.pop()
else:
self.lrnCramQueue.pop()
def _rebuildCramNewCount(self):
self.newAvail = 0
self.newCount = 0
def _cramCardLimit(self, active, inactive, sql):
# inactive is (currently) ignored
if isinstance(active, list):
return sql.replace(
"where", "where +c.id in " + ids2str(active) + " and")
else:
yes = parseTags(active)
if yes:
yids = tagIds(self.db, yes).values()
return sql.replace(
"where ",
"where +c.id in (select cardId from cardTags where "
"tagId in %s) and " % ids2str(yids))
else:
return sql
def _fillCramQueue(self):
if self.revCount and not self.revQueue:
self.revQueue = self.db.all(self.cardLimit(
self.activeCramTags, "", """
select id, factId from cards c
where queue between 0 and 2
order by %s
limit %s""" % (self.cramOrder, self.queueLimit)))
self.revQueue.reverse()
def _rebuildCramCount(self):
self.revCount = self.db.scalar(self.cardLimit(
self.activeCramTags, "",
"select count(*) from cards c where queue between 0 and 2"))
def _rebuildLrnCramCount(self):
self.lrnCount = len(self.lrnCramQueue)
def _fillLrnCramQueue(self):
self.lrnQueue = self.lrnCramQueue

View file

@ -37,9 +37,11 @@ def upgradeSchema(engine, s):
import cards
metadata.create_all(engine, tables=[cards.cardsTable])
s.execute("""
insert into cards select id, factId, cardModelId, created, modified,
question, answer, 0, ordinal, 0, relativeDelay, type, lastInterval, interval,
due, factor, reps, successive, noCount from cards2""")
insert into cards select id, factId,
(select modelId from facts where facts.id = cards.factId),
cardModelId, created, modified,
question, answer, ordinal, 0, relativeDelay, type, due, interval,
factor, reps, successive, noCount, 0, 0 from cards2""")
s.execute("drop table cards2")
# tags
###########

View file

@ -1,3 +1,6 @@
import tempfile, os
from anki import Deck
def assertException(exception, func):
found = False
try:
@ -5,3 +8,8 @@ def assertException(exception, func):
except exception:
found = True
assert found
def getDeck():
(fd, nam) = tempfile.mkstemp(suffix=".anki")
os.unlink(nam)
return Deck(nam)

View file

@ -1,7 +1,7 @@
# coding: utf-8
import nose, os, re
from tests.shared import assertException
from tests.shared import assertException, getDeck
from anki.errors import *
from anki import Deck
@ -15,12 +15,6 @@ newModified = None
testDir = os.path.dirname(__file__)
def getDeck():
import tempfile
(fd, nam) = tempfile.mkstemp(suffix=".anki")
os.unlink(nam)
return Deck(nam)
## opening/closing
def test_attachNew():

62
tests/test_sched.py Normal file
View file

@ -0,0 +1,62 @@
# coding: utf-8
import time
from tests.shared import assertException, getDeck
from anki.stdmodels import BasicModel
#from anki.db import *
def getEmptyDeck():
d = getDeck()
d.addModel(BasicModel())
d.db.commit()
return d
def test_basics():
d = getEmptyDeck()
assert not d.getCard()
def test_learn():
d = getEmptyDeck()
# add a fact
f = d.newFact()
f['Front'] = u"one"; f['Back'] = u"two"
f = d.addFact(f)
d.db.flush()
# set as a learn card and rebuild queues
d.db.statement("update cards set queue=0, type=2")
d.reset()
# getCard should return it, since it's due in the past
c = d.getCard()
assert c
# it should have no cycles and a grade of 0
assert c.grade == c.cycles == 0
# fail it
d.answerCard(c, 1)
# it should by due in 30 seconds
assert round(c.due - time.time()) == 30
# and have 1 cycle, but still a zero grade
assert c.grade == 0
assert c.cycles == 1
# pass it once
d.answerCard(c, 2)
# it should by due in 3 minutes
assert round(c.due - time.time()) == 180
# and it should be grade 1 now
assert c.grade == 1
assert c.cycles == 2
# pass again
d.answerCard(c, 2)
# it should by due in 10 minutes
assert round(c.due - time.time()) == 600
# and it should be grade 1 now
assert c.grade == 2
assert c.cycles == 3
# the next pass should graduate the card
assert c.queue == 0
assert c.type == 2
d.answerCard(c, 2)
assert c.queue == 1
assert c.type == 1
print "test intervals, check early removal, etc"