mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
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:
parent
4e7e8b03bc
commit
b0b4074cbd
9 changed files with 479 additions and 381 deletions
|
@ -14,46 +14,39 @@ MAX_TIMER = 60
|
||||||
# Cards
|
# Cards
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# tasks:
|
# Type: 0=learning, 1=due, 2=new
|
||||||
# - remove all failed cards from learning queue - set queue=1; type=1 and
|
# Queue: 0=learning, 1=due, 2=new
|
||||||
# leave scheduling parameters alone (need separate due for learn queue and
|
# -1=suspended, -2=user buried, -3=sched buried
|
||||||
# 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)
|
|
||||||
# Ordinal: card template # for fact
|
# Ordinal: card template # for fact
|
||||||
# Position: sorting position, only for new cards
|
|
||||||
# Flags: unused; reserved for future use
|
# Flags: unused; reserved for future use
|
||||||
|
|
||||||
cardsTable = Table(
|
cardsTable = Table(
|
||||||
'cards', metadata,
|
'cards', metadata,
|
||||||
Column('id', Integer, primary_key=True),
|
Column('id', Integer, primary_key=True),
|
||||||
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
|
Column('factId', Integer, ForeignKey("facts.id"), nullable=False),
|
||||||
|
Column('modelId', Integer, ForeignKey("models.id"), nullable=False),
|
||||||
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
|
Column('cardModelId', Integer, ForeignKey("cardModels.id"), nullable=False),
|
||||||
# general
|
# general
|
||||||
Column('created', Float, nullable=False, default=time.time),
|
Column('created', Float, nullable=False, default=time.time),
|
||||||
Column('modified', Float, nullable=False, default=time.time),
|
Column('modified', Float, nullable=False, default=time.time),
|
||||||
Column('question', UnicodeText, nullable=False, default=u""),
|
Column('question', UnicodeText, nullable=False, default=u""),
|
||||||
Column('answer', 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('ordinal', Integer, nullable=False),
|
||||||
Column('position', Integer, nullable=False),
|
Column('flags', Integer, nullable=False, default=0),
|
||||||
# scheduling data
|
# shared scheduling
|
||||||
Column('type', Integer, nullable=False, default=2),
|
Column('type', Integer, nullable=False, default=2),
|
||||||
Column('queue', 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),
|
Column('due', Float, nullable=False),
|
||||||
|
# sm2
|
||||||
|
Column('interval', Float, nullable=False, default=0),
|
||||||
Column('factor', Float, nullable=False, default=2.5),
|
Column('factor', Float, nullable=False, default=2.5),
|
||||||
# counters
|
|
||||||
Column('reps', Integer, nullable=False, default=0),
|
Column('reps', Integer, nullable=False, default=0),
|
||||||
Column('successive', Integer, nullable=False, default=0),
|
Column('streak', Integer, nullable=False, default=0),
|
||||||
Column('lapses', 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):
|
class Card(object):
|
||||||
|
|
||||||
|
@ -68,6 +61,7 @@ class Card(object):
|
||||||
self.position = self.due
|
self.position = self.due
|
||||||
if fact:
|
if fact:
|
||||||
self.fact = fact
|
self.fact = fact
|
||||||
|
self.modelId = fact.modelId
|
||||||
if cardModel:
|
if cardModel:
|
||||||
self.cardModel = cardModel
|
self.cardModel = cardModel
|
||||||
# for non-orm use
|
# for non-orm use
|
||||||
|
@ -151,45 +145,47 @@ class Card(object):
|
||||||
return
|
return
|
||||||
(self.id,
|
(self.id,
|
||||||
self.factId,
|
self.factId,
|
||||||
|
self.modelId,
|
||||||
self.cardModelId,
|
self.cardModelId,
|
||||||
self.created,
|
self.created,
|
||||||
self.modified,
|
self.modified,
|
||||||
self.question,
|
self.question,
|
||||||
self.answer,
|
self.answer,
|
||||||
self.flags,
|
|
||||||
self.ordinal,
|
self.ordinal,
|
||||||
self.position,
|
self.flags,
|
||||||
self.type,
|
self.type,
|
||||||
self.queue,
|
self.queue,
|
||||||
self.lastInterval,
|
|
||||||
self.interval,
|
|
||||||
self.due,
|
self.due,
|
||||||
|
self.interval,
|
||||||
self.factor,
|
self.factor,
|
||||||
self.reps,
|
self.reps,
|
||||||
self.successive,
|
self.streak,
|
||||||
self.lapses) = r
|
self.lapses,
|
||||||
|
self.grade,
|
||||||
|
self.cycles) = r
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def toDB(self, s):
|
def toDB(self, s):
|
||||||
s.execute("""update cards set
|
s.execute("""update cards set
|
||||||
factId=:factId,
|
factId=:factId,
|
||||||
|
modelId=:modelId,
|
||||||
cardModelId=:cardModelId,
|
cardModelId=:cardModelId,
|
||||||
created=:created,
|
created=:created,
|
||||||
modified=:modified,
|
modified=:modified,
|
||||||
question=:question,
|
question=:question,
|
||||||
answer=:answer,
|
answer=:answer,
|
||||||
flags=:flags,
|
|
||||||
ordinal=:ordinal,
|
ordinal=:ordinal,
|
||||||
position=:position,
|
flags=:flags,
|
||||||
type=:type,
|
type=:type,
|
||||||
queue=:queue,
|
queue=:queue,
|
||||||
lastInterval=:lastInterval,
|
|
||||||
interval=:interval,
|
|
||||||
due=:due,
|
due=:due,
|
||||||
|
interval=:interval,
|
||||||
factor=:factor,
|
factor=:factor,
|
||||||
reps=:reps,
|
reps=:reps,
|
||||||
successive=:successive,
|
streak=:streak,
|
||||||
lapses=:lapses
|
lapses=:lapses,
|
||||||
|
grade=:grade,
|
||||||
|
cycles=:cycles
|
||||||
where id=:id""", self.__dict__)
|
where id=:id""", self.__dict__)
|
||||||
|
|
||||||
mapper(Card, cardsTable, properties={
|
mapper(Card, cardsTable, properties={
|
||||||
|
|
118
anki/cram.py
Normal file
118
anki/cram.py
Normal 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
|
|
@ -133,6 +133,9 @@ class Deck(object):
|
||||||
|
|
||||||
def getCard(self):
|
def getCard(self):
|
||||||
return self.sched.getCard()
|
return self.sched.getCard()
|
||||||
|
|
||||||
|
def answerCard(self, card, ease):
|
||||||
|
self.sched.answerCard(card, ease)
|
||||||
# if card:
|
# if card:
|
||||||
# return card
|
# return card
|
||||||
# if sched.name == "main":
|
# if sched.name == "main":
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
# 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 sqlalchemy.ext.orderinglist import ordering_list
|
||||||
from anki.db import *
|
from anki.db import *
|
||||||
from anki.utils import genID, canonifyTags
|
from anki.utils import genID, canonifyTags
|
||||||
|
@ -153,9 +153,6 @@ def formatQA(cid, mid, fact, tags, cm, deck):
|
||||||
# Model table
|
# 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
|
# 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
|
# 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
|
# 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
|
# optional, what intervals should the default be? 3 days or more if cards are
|
||||||
# over that interval range? and what about failed mature bonus?
|
# 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(
|
modelsTable = Table(
|
||||||
'models', metadata,
|
'models', metadata,
|
||||||
Column('id', Integer, primary_key=True),
|
Column('id', Integer, primary_key=True),
|
||||||
Column('created', Float, nullable=False, default=time.time),
|
Column('created', Float, nullable=False, default=time.time),
|
||||||
Column('modified', Float, nullable=False, default=time.time),
|
Column('modified', Float, nullable=False, default=time.time),
|
||||||
Column('name', UnicodeText, nullable=False),
|
Column('name', UnicodeText, nullable=False),
|
||||||
# new cards
|
Column('config', UnicodeText, nullable=False,
|
||||||
Column('newSched', UnicodeText, nullable=False, default=u"[0.5, 3, 10]"),
|
default=unicode(simplejson.dumps(defaultConf))),
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Model(object):
|
class Model(object):
|
||||||
"Defines the way a fact behaves, what fields it can contain, etc."
|
"Defines the way a fact behaves, what fields it can contain, etc."
|
||||||
def __init__(self, name=u""):
|
def __init__(self, name=u""):
|
||||||
|
|
558
anki/sched.py
558
anki/sched.py
|
@ -2,11 +2,12 @@
|
||||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
# 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 heapq import *
|
||||||
from anki.db import *
|
from anki.db import *
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.utils import parseTags
|
from anki.utils import parseTags
|
||||||
|
from anki.lang import _
|
||||||
|
|
||||||
# the standard Anki scheduler
|
# the standard Anki scheduler
|
||||||
class Scheduler(object):
|
class Scheduler(object):
|
||||||
|
@ -26,106 +27,57 @@ class Scheduler(object):
|
||||||
|
|
||||||
def getCard(self, orm=True):
|
def getCard(self, orm=True):
|
||||||
"Pop the next card from the queue. None if finished."
|
"Pop the next card from the queue. None if finished."
|
||||||
id = self._getCard()
|
self.checkDay()
|
||||||
|
id = self.getCardId()
|
||||||
if id:
|
if id:
|
||||||
card = Card()
|
card = Card()
|
||||||
assert card.fromDB(self.db, id)
|
assert card.fromDB(self.db, id)
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
self.modelConfigs = {}
|
||||||
self.resetLearn()
|
self.resetLearn()
|
||||||
self.resetReview()
|
self.resetReview()
|
||||||
self.resetNew()
|
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 answerCard(self, card, ease):
|
||||||
def rebuildTypes(self):
|
if card.queue == 0:
|
||||||
"Rebuild the type cache. Only necessary on upgrade."
|
self.answerLearnCard(card, ease)
|
||||||
# set type first
|
elif card.queue == 1:
|
||||||
self.db.statement("""
|
self.answerRevCard(card, ease)
|
||||||
update cards set type = (case
|
else:
|
||||||
when successive then 1 when reps then 0 else 2 end)
|
raise Exception("Invalid queue")
|
||||||
""")
|
card.toDB(self.db)
|
||||||
# then queue
|
|
||||||
self.db.statement("""
|
def counts(self):
|
||||||
update cards set queue = type
|
# FIXME: should learn count include new cards due today, or be separate?
|
||||||
when queue != -1""")
|
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):
|
def cardQueue(self, card):
|
||||||
return self.cardType(card)
|
return card.queue
|
||||||
|
|
||||||
def cardType(self, card):
|
# Getting the next 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
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def resetSchedBuried(self):
|
def getCardId(self):
|
||||||
"Put temporarily suspended cards back into play."
|
"Return the next due card id, or None."
|
||||||
self.db.statement(
|
# learning card due?
|
||||||
"update cards set queue = type where queue = -3")
|
id = self.getLearnCard()
|
||||||
|
if id:
|
||||||
def cardLimit(self, active, inactive, sql):
|
return id
|
||||||
yes = parseTags(getattr(self.deck, active))
|
# new first, or time for one?
|
||||||
no = parseTags(getattr(self.deck, inactive))
|
if self.timeForNewCard():
|
||||||
if yes:
|
return self.getNewCard()
|
||||||
yids = tagIds(self.db, yes).values()
|
# card due for review?
|
||||||
nids = tagIds(self.db, no).values()
|
id = self.getReviewCard()
|
||||||
return sql.replace(
|
if id:
|
||||||
"where",
|
return id
|
||||||
"where +c.id in (select cardId from cardTags where "
|
# new cards left?
|
||||||
"tagId in %s) and +c.id not in (select cardId from "
|
id = self.getNewCard()
|
||||||
"cardTags where tagId in %s) and" % (
|
if id:
|
||||||
ids2str(yids),
|
return id
|
||||||
ids2str(nids)))
|
# collapse or finish
|
||||||
elif no:
|
return self.getLearnCard(collapse=True)
|
||||||
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()
|
|
||||||
|
|
||||||
# Learning queue
|
# Learning queue
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -135,12 +87,64 @@ when queue != -1""")
|
||||||
select due, id from cards where
|
select due, id from cards where
|
||||||
queue = 0 and due < :lim order by due
|
queue = 0 and due < :lim order by due
|
||||||
limit %d""" % self.learnLimit, lim=self.dayCutoff)
|
limit %d""" % self.learnLimit, lim=self.dayCutoff)
|
||||||
self.learnQueue.reverse()
|
|
||||||
self.learnCount = len(self.learnQueue)
|
self.learnCount = len(self.learnQueue)
|
||||||
|
|
||||||
def getLearnCard(self):
|
def getLearnCard(self, collapse=False):
|
||||||
if self.learnQueue and self.learnQueue[0] < time.time():
|
if self.learnQueue:
|
||||||
return heappop(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
|
# Reviews
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -183,128 +187,10 @@ limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dayCutoff)
|
||||||
def showFailedLast(self):
|
def showFailedLast(self):
|
||||||
return self.collapseTime or not self.delay0
|
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
|
# Answering a card
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def answerCard(self, card, ease):
|
def _answerCard(self, card, ease):
|
||||||
undoName = _("Answer Card")
|
undoName = _("Answer Card")
|
||||||
self.setUndoStart(undoName)
|
self.setUndoStart(undoName)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
@ -520,6 +406,148 @@ and queue between 1 and 2""",
|
||||||
self.reset()
|
self.reset()
|
||||||
self.refreshSession()
|
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
|
# Review early
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -571,115 +599,3 @@ order by due limit %d""" % self.queueLimit), lim=self.dayCutoff)
|
||||||
|
|
||||||
def _updateLearnMoreCountToday(self):
|
def _updateLearnMoreCountToday(self):
|
||||||
self.newCount = self.newAvail
|
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
|
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,11 @@ def upgradeSchema(engine, s):
|
||||||
import cards
|
import cards
|
||||||
metadata.create_all(engine, tables=[cards.cardsTable])
|
metadata.create_all(engine, tables=[cards.cardsTable])
|
||||||
s.execute("""
|
s.execute("""
|
||||||
insert into cards select id, factId, cardModelId, created, modified,
|
insert into cards select id, factId,
|
||||||
question, answer, 0, ordinal, 0, relativeDelay, type, lastInterval, interval,
|
(select modelId from facts where facts.id = cards.factId),
|
||||||
due, factor, reps, successive, noCount from cards2""")
|
cardModelId, created, modified,
|
||||||
|
question, answer, ordinal, 0, relativeDelay, type, due, interval,
|
||||||
|
factor, reps, successive, noCount, 0, 0 from cards2""")
|
||||||
s.execute("drop table cards2")
|
s.execute("drop table cards2")
|
||||||
# tags
|
# tags
|
||||||
###########
|
###########
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import tempfile, os
|
||||||
|
from anki import Deck
|
||||||
|
|
||||||
def assertException(exception, func):
|
def assertException(exception, func):
|
||||||
found = False
|
found = False
|
||||||
try:
|
try:
|
||||||
|
@ -5,3 +8,8 @@ def assertException(exception, func):
|
||||||
except exception:
|
except exception:
|
||||||
found = True
|
found = True
|
||||||
assert found
|
assert found
|
||||||
|
|
||||||
|
def getDeck():
|
||||||
|
(fd, nam) = tempfile.mkstemp(suffix=".anki")
|
||||||
|
os.unlink(nam)
|
||||||
|
return Deck(nam)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import nose, os, re
|
import nose, os, re
|
||||||
from tests.shared import assertException
|
from tests.shared import assertException, getDeck
|
||||||
|
|
||||||
from anki.errors import *
|
from anki.errors import *
|
||||||
from anki import Deck
|
from anki import Deck
|
||||||
|
@ -15,12 +15,6 @@ newModified = None
|
||||||
|
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
|
|
||||||
def getDeck():
|
|
||||||
import tempfile
|
|
||||||
(fd, nam) = tempfile.mkstemp(suffix=".anki")
|
|
||||||
os.unlink(nam)
|
|
||||||
return Deck(nam)
|
|
||||||
|
|
||||||
## opening/closing
|
## opening/closing
|
||||||
|
|
||||||
def test_attachNew():
|
def test_attachNew():
|
||||||
|
|
62
tests/test_sched.py
Normal file
62
tests/test_sched.py
Normal 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"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue