mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
more group/sched refactoring
- keep track of rep/time counts per group, instead of just at the top level - sort by due after retrieving learn cards - ensure activeGroups is sorted alphabetically - ensure new cards come in alphabetical group order - ensure queues are refilled when empty
This commit is contained in:
parent
024c42fef8
commit
2b34d8a948
5 changed files with 319 additions and 161 deletions
127
anki/groups.py
127
anki/groups.py
|
@ -2,9 +2,10 @@
|
||||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import simplejson
|
import simplejson, copy
|
||||||
from anki.utils import intTime, ids2str
|
from anki.utils import intTime, ids2str
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
from anki.lang import _
|
||||||
|
|
||||||
# fixmes:
|
# fixmes:
|
||||||
# - make sure lists like new[delays] are not being shared by multiple groups
|
# - make sure lists like new[delays] are not being shared by multiple groups
|
||||||
|
@ -19,14 +20,19 @@ from anki.consts import *
|
||||||
# references, and treat any invalid gids as the default group.
|
# references, and treat any invalid gids as the default group.
|
||||||
# - deletions of group config force a full sync
|
# - deletions of group config force a full sync
|
||||||
|
|
||||||
# configuration only available to top level groups
|
# these are a cache of the current day's reviews. they may be wrong after a
|
||||||
defaultTopConf = {
|
# sync merge if someone reviewed from two locations
|
||||||
'newPerDay': 20,
|
defaultGroup = {
|
||||||
'newToday': [0, 0], # currentDay, count
|
'newToday': [0, 0], # currentDay, count
|
||||||
'revToday': [0, 0],
|
'revToday': [0, 0],
|
||||||
'lrnToday': [0, 0],
|
'lrnToday': [0, 0],
|
||||||
'timeToday': [0, 0], # currentDay, time in ms
|
'timeToday': [0, 0], # time in ms
|
||||||
'newTodayOrder': NEW_TODAY_ORD,
|
'conf': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
# configuration only available to top level groups
|
||||||
|
defaultTopConf = {
|
||||||
|
'revLim': 100,
|
||||||
'newSpread': NEW_CARDS_DISTRIBUTE,
|
'newSpread': NEW_CARDS_DISTRIBUTE,
|
||||||
'collapseTime': 1200,
|
'collapseTime': 1200,
|
||||||
'repLim': 0,
|
'repLim': 0,
|
||||||
|
@ -40,6 +46,8 @@ defaultConf = {
|
||||||
'delays': [1, 10],
|
'delays': [1, 10],
|
||||||
'ints': [1, 7, 4],
|
'ints': [1, 7, 4],
|
||||||
'initialFactor': 2500,
|
'initialFactor': 2500,
|
||||||
|
'order': NEW_TODAY_ORD,
|
||||||
|
'perDay': 20,
|
||||||
},
|
},
|
||||||
'lapse': {
|
'lapse': {
|
||||||
'delays': [1, 10],
|
'delays': [1, 10],
|
||||||
|
@ -110,8 +118,9 @@ class GroupManager(object):
|
||||||
# not top level; ensure all parents exist
|
# not top level; ensure all parents exist
|
||||||
g = {}
|
g = {}
|
||||||
self._ensureParents(name)
|
self._ensureParents(name)
|
||||||
|
for (k,v) in defaultGroup.items():
|
||||||
|
g[k] = v
|
||||||
g['name'] = name
|
g['name'] = name
|
||||||
g['conf'] = 1
|
|
||||||
while 1:
|
while 1:
|
||||||
id = intTime(1000)
|
id = intTime(1000)
|
||||||
if str(id) not in self.groups:
|
if str(id) not in self.groups:
|
||||||
|
@ -119,7 +128,7 @@ class GroupManager(object):
|
||||||
g['id'] = id
|
g['id'] = id
|
||||||
self.groups[str(id)] = g
|
self.groups[str(id)] = g
|
||||||
self.save(g)
|
self.save(g)
|
||||||
self.maybeAddToActive(g)
|
self.maybeAddToActive()
|
||||||
return int(id)
|
return int(id)
|
||||||
|
|
||||||
def rem(self, gid, cardsToo=False):
|
def rem(self, gid, cardsToo=False):
|
||||||
|
@ -142,9 +151,19 @@ class GroupManager(object):
|
||||||
"A list of all groups."
|
"A list of all groups."
|
||||||
return self.groups.values()
|
return self.groups.values()
|
||||||
|
|
||||||
def allConf(self):
|
def get(self, gid, default=True):
|
||||||
"A list of all group config."
|
id = str(gid)
|
||||||
return self.gconf.values()
|
if id in self.groups:
|
||||||
|
return self.groups[id]
|
||||||
|
elif default:
|
||||||
|
return self.groups['1']
|
||||||
|
|
||||||
|
def update(self, g):
|
||||||
|
"Add or update an existing group. Used for syncing and merging."
|
||||||
|
self.groups[str(g['id'])] = g
|
||||||
|
self.maybeAddToActive()
|
||||||
|
# mark registry changed, but don't bump mod time
|
||||||
|
self.save()
|
||||||
|
|
||||||
def _ensureParents(self, name):
|
def _ensureParents(self, name):
|
||||||
path = name.split("::")
|
path = name.split("::")
|
||||||
|
@ -156,42 +175,61 @@ class GroupManager(object):
|
||||||
s += "::" + p
|
s += "::" + p
|
||||||
self.id(s)
|
self.id(s)
|
||||||
|
|
||||||
|
# Group configurations
|
||||||
|
#############################################################
|
||||||
|
|
||||||
|
def allConf(self):
|
||||||
|
"A list of all group config."
|
||||||
|
return self.gconf.values()
|
||||||
|
|
||||||
|
def conf(self, gid):
|
||||||
|
return self.gconf[str(self.groups[str(gid)]['conf'])]
|
||||||
|
|
||||||
|
def updateConf(self, g):
|
||||||
|
self.gconf[str(g['id'])] = g
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def confId(self, name):
|
||||||
|
"Create a new configuration and return id."
|
||||||
|
c = copy.deepcopy(defaultConf)
|
||||||
|
while 1:
|
||||||
|
id = intTime(1000)
|
||||||
|
if str(id) not in self.gconf:
|
||||||
|
break
|
||||||
|
c['id'] = id
|
||||||
|
self.gconf[str(id)] = c
|
||||||
|
self.save(c)
|
||||||
|
return id
|
||||||
|
|
||||||
|
def remConf(self, id):
|
||||||
|
"Remove a configuration and update all groups using it."
|
||||||
|
self.deck.modSchema()
|
||||||
|
del self.gconf[str(id)]
|
||||||
|
for g in self.all():
|
||||||
|
if g['conf'] == id:
|
||||||
|
g['conf'] = 1
|
||||||
|
self.save(g)
|
||||||
|
|
||||||
|
def setConf(self, grp, id):
|
||||||
|
grp['conf'] = id
|
||||||
|
self.save(grp)
|
||||||
|
|
||||||
# Group utils
|
# Group utils
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def name(self, gid):
|
def name(self, gid):
|
||||||
return self.get(gid)['name']
|
return self.get(gid)['name']
|
||||||
|
|
||||||
def conf(self, gid):
|
|
||||||
return self.gconf[str(self.groups[str(gid)]['conf'])]
|
|
||||||
|
|
||||||
def get(self, gid, default=True):
|
|
||||||
id = str(gid)
|
|
||||||
if id in self.groups:
|
|
||||||
return self.groups[id]
|
|
||||||
elif default:
|
|
||||||
return self.groups['1']
|
|
||||||
|
|
||||||
def setGroup(self, cids, gid):
|
def setGroup(self, cids, gid):
|
||||||
self.deck.db.execute(
|
self.deck.db.execute(
|
||||||
"update cards set gid=?,usn=?,mod=? where id in "+
|
"update cards set gid=?,usn=?,mod=? where id in "+
|
||||||
ids2str(cids), gid, self.deck.usn(), intTime())
|
ids2str(cids), gid, self.deck.usn(), intTime())
|
||||||
|
|
||||||
def update(self, g):
|
|
||||||
"Add or update an existing group. Used for syncing and merging."
|
|
||||||
self.groups[str(g['id'])] = g
|
|
||||||
self.maybeAddToActive(g)
|
|
||||||
# mark registry changed, but don't bump mod time
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def maybeAddToActive(self, g):
|
def maybeAddToActive(self):
|
||||||
# since order is important, we can't just append to the end
|
# since order is important, we can't just append to the end
|
||||||
self.select(self.selected())
|
self.select(self.selected())
|
||||||
|
|
||||||
def updateConf(self, g):
|
|
||||||
self.gconf[str(g['id'])] = g
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def sendHome(self, cids):
|
def sendHome(self, cids):
|
||||||
self.deck.db.execute("""
|
self.deck.db.execute("""
|
||||||
update cards set gid=(select gid from facts f where f.id=fid),
|
update cards set gid=(select gid from facts f where f.id=fid),
|
||||||
|
@ -205,9 +243,8 @@ usn=?,mod=? where id in %s""" % ids2str(cids),
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def top(self):
|
def top(self):
|
||||||
"The current top level group as an object, and marks as modified."
|
"The current top level group as an object."
|
||||||
g = self.get(self.deck.conf['topGroup'])
|
g = self.get(self.deck.conf['topGroup'])
|
||||||
self.save(g)
|
|
||||||
return g
|
return g
|
||||||
|
|
||||||
def active(self):
|
def active(self):
|
||||||
|
@ -222,23 +259,23 @@ usn=?,mod=? where id in %s""" % ids2str(cids),
|
||||||
"Select a new branch."
|
"Select a new branch."
|
||||||
# save the top level group
|
# save the top level group
|
||||||
name = self.groups[str(gid)]['name']
|
name = self.groups[str(gid)]['name']
|
||||||
self.deck.conf['topGroup'] = self.topFor(name)
|
self.deck.conf['topGroup'] = self._topFor(name)
|
||||||
# current group
|
# current group
|
||||||
self.deck.conf['curGroup'] = gid
|
self.deck.conf['curGroup'] = gid
|
||||||
# and active groups (current + all children)
|
# and active groups (current + all children)
|
||||||
actv = [gid]
|
actv = []
|
||||||
for g in self.all():
|
for g in self.all():
|
||||||
if g['name'].startswith(name + "::"):
|
if g['name'].startswith(name + "::"):
|
||||||
actv.append(g['id'])
|
actv.append((g['name'], g['id']))
|
||||||
self.deck.conf['activeGroups'] = actv
|
actv.sort()
|
||||||
|
self.deck.conf['activeGroups'] = [gid] + [a[1] for a in actv]
|
||||||
|
|
||||||
def topFor(self, name):
|
def parents(self, gid):
|
||||||
|
"All parents of gid."
|
||||||
|
path = self.get(gid)['name'].split("::")
|
||||||
|
return [self.get(x) for x in path[:-1]]
|
||||||
|
|
||||||
|
def _topFor(self, name):
|
||||||
"The top level gid for NAME."
|
"The top level gid for NAME."
|
||||||
path = name.split("::")
|
path = name.split("::")
|
||||||
return self.id(path[0])
|
return self.id(path[0])
|
||||||
|
|
||||||
def underSelected(self, name):
|
|
||||||
"True if name is under the selected group."
|
|
||||||
# if nothing is selected, always true
|
|
||||||
s = self.selected()
|
|
||||||
return name.startswith(self.get(s)['name'])
|
|
||||||
|
|
|
@ -99,7 +99,9 @@ class ModelManager(object):
|
||||||
return self.models.values()[0]
|
return self.models.values()[0]
|
||||||
|
|
||||||
def setCurrent(self, m):
|
def setCurrent(self, m):
|
||||||
self.deck.groups.top()['curModel'] = m['id']
|
t = self.deck.groups.top()
|
||||||
|
t['curModel'] = m['id']
|
||||||
|
self.deck.groups.save(t)
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"Get model with ID, or None."
|
"Get model with ID, or None."
|
||||||
|
|
230
anki/sched.py
230
anki/sched.py
|
@ -20,8 +20,9 @@ class Scheduler(object):
|
||||||
name = "std"
|
name = "std"
|
||||||
def __init__(self, deck):
|
def __init__(self, deck):
|
||||||
self.deck = deck
|
self.deck = deck
|
||||||
self.queueLimit = 200
|
self.queueLimit = 50
|
||||||
self.reportLimit = 1000
|
self.reportLimit = 1000
|
||||||
|
# fixme: replace reps with group based counts
|
||||||
self.reps = 0
|
self.reps = 0
|
||||||
self._updateCutoff()
|
self._updateCutoff()
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class Scheduler(object):
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._resetCounts()
|
self._updateCutoff()
|
||||||
self._resetLrn()
|
self._resetLrn()
|
||||||
self._resetRev()
|
self._resetRev()
|
||||||
self._resetNew()
|
self._resetNew()
|
||||||
|
@ -50,20 +51,23 @@ class Scheduler(object):
|
||||||
# put it in the learn queue
|
# put it in the learn queue
|
||||||
card.queue = 1
|
card.queue = 1
|
||||||
card.type = 1
|
card.type = 1
|
||||||
self._updateStats('new')
|
self._updateStats(card, 'new')
|
||||||
if card.queue == 1:
|
if card.queue == 1:
|
||||||
self._answerLrnCard(card, ease)
|
self._answerLrnCard(card, ease)
|
||||||
if not wasNew:
|
if not wasNew:
|
||||||
self._updateStats('lrn')
|
self._updateStats(card, 'lrn')
|
||||||
elif card.queue == 2:
|
elif card.queue == 2:
|
||||||
self._answerRevCard(card, ease)
|
self._answerRevCard(card, ease)
|
||||||
self._updateStats('rev')
|
self._updateStats(card, 'rev')
|
||||||
else:
|
else:
|
||||||
raise Exception("Invalid queue")
|
raise Exception("Invalid queue")
|
||||||
self._updateStats('time', card.timeTaken())
|
self._updateStats(card, 'time', card.timeTaken())
|
||||||
card.mod = intTime()
|
card.mod = intTime()
|
||||||
card.usn = self.deck.usn()
|
card.usn = self.deck.usn()
|
||||||
card.flushSched()
|
card.flushSched()
|
||||||
|
# if nothing more to study, rebuild queue
|
||||||
|
if self.counts() == (0,0,0):
|
||||||
|
self.reset()
|
||||||
|
|
||||||
def counts(self):
|
def counts(self):
|
||||||
"Does not include fetched but unanswered."
|
"Does not include fetched but unanswered."
|
||||||
|
@ -101,24 +105,19 @@ order by due""" % self._groupLimit(),
|
||||||
self.deck.db.execute(
|
self.deck.db.execute(
|
||||||
"update cards set queue = type where queue between -3 and -2")
|
"update cards set queue = type where queue between -3 and -2")
|
||||||
|
|
||||||
# Counts
|
# Rev/lrn/time daily stats
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _resetCounts(self):
|
def _updateStats(self, card, type, cnt=1):
|
||||||
self._updateCutoff()
|
key = type+"Today"
|
||||||
self._resetLrnCount()
|
for g in ([self.deck.groups.get(card.gid)] +
|
||||||
self._resetRevCount()
|
self.deck.groups.parents(card.gid)):
|
||||||
self._resetNewCount()
|
# ensure we're on the correct day
|
||||||
self._updateStatsDay("time")
|
if g[key][0] != self.today:
|
||||||
|
g[key] = [self.today, 0]
|
||||||
def _updateStatsDay(self, type):
|
# add
|
||||||
l = self.deck.groups.top()
|
g[key][1] += cnt
|
||||||
if l[type+'Today'][0] != self.today:
|
self.deck.groups.save(g)
|
||||||
# it's a new day; reset counts
|
|
||||||
l[type+'Today'] = [self.today, 0]
|
|
||||||
|
|
||||||
def _updateStats(self, type, cnt=1):
|
|
||||||
self.deck.groups.top()[type+'Today'][1] += cnt
|
|
||||||
|
|
||||||
# Group counts
|
# Group counts
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -128,22 +127,8 @@ order by due""" % self._groupLimit(),
|
||||||
# find groups with 1 or more due cards
|
# find groups with 1 or more due cards
|
||||||
gids = {}
|
gids = {}
|
||||||
for g in self.deck.groups.all():
|
for g in self.deck.groups.all():
|
||||||
hasDue = self.deck.db.scalar("""
|
hasDue = self._groupHasLrn(g['id']) or self._groupHasRev(g['id'])
|
||||||
select 1 from cards where gid = ? and
|
hasNew = self._groupHasNew(g['id'])
|
||||||
((queue = 2 and due <= ?) or (queue = 1 and due < ?)) limit 1""",
|
|
||||||
g['id'], self.today, intTime())
|
|
||||||
top = self.deck.groups.get(
|
|
||||||
self.deck.groups.topFor(g['name']))
|
|
||||||
if top['newToday'][0] != self.today:
|
|
||||||
# it's a new day; reset counts
|
|
||||||
top['newToday'] = [self.today, 0]
|
|
||||||
hasNew = max(0, top['newPerDay'] - top['newToday'][1])
|
|
||||||
if hasNew:
|
|
||||||
# if the limit hasn't run out, check to see if there are
|
|
||||||
# actually cards
|
|
||||||
hasNew = self.deck.db.scalar(
|
|
||||||
"select 1 from cards where queue = 0 and gid = ? limit 1",
|
|
||||||
g['id'])
|
|
||||||
gids[g['id']] = [hasDue or 0, hasNew or 0]
|
gids[g['id']] = [hasDue or 0, hasNew or 0]
|
||||||
return [[grp['name'], int(gid)]+gids.get(int(gid))
|
return [[grp['name'], int(gid)]+gids.get(int(gid))
|
||||||
for (gid, grp) in self._orderedGroups()]
|
for (gid, grp) in self._orderedGroups()]
|
||||||
|
@ -222,37 +207,68 @@ select 1 from cards where gid = ? and
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _resetNewCount(self):
|
def _resetNewCount(self):
|
||||||
self._updateStatsDay("new")
|
|
||||||
l = self.deck.groups.top()
|
|
||||||
lim = min(self.reportLimit, l['newPerDay'] - l['newToday'][1])
|
|
||||||
if lim <= 0:
|
|
||||||
self.newCount = 0
|
self.newCount = 0
|
||||||
else:
|
pcounts = {}
|
||||||
self.newCount = self.deck.db.scalar("""
|
# for each of the active groups
|
||||||
select count() from (select id from cards where
|
for gid in self.deck.groups.active():
|
||||||
gid in %s and queue = 0 limit %d)""" % (self._groupLimit(), lim))
|
# get the individual group's limit
|
||||||
|
lim = self._groupNewLimitSingle(self.deck.groups.get(gid))
|
||||||
|
if not lim:
|
||||||
|
continue
|
||||||
|
# check the parents
|
||||||
|
parents = self.deck.groups.parents(gid)
|
||||||
|
for p in parents:
|
||||||
|
# add if missing
|
||||||
|
if p['id'] not in pcounts:
|
||||||
|
pcounts[p['id']] = self._groupNewLimitSingle(p)
|
||||||
|
# take minimum of child and parent
|
||||||
|
lim = min(pcounts[p['id']], lim)
|
||||||
|
# see how many cards we actually have
|
||||||
|
cnt = self.deck.db.scalar("""
|
||||||
|
select count() from (select 1 from cards where
|
||||||
|
gid = ? and queue = 0 limit ?)""", gid, lim)
|
||||||
|
# if non-zero, decrement from parent counts
|
||||||
|
for p in parents:
|
||||||
|
pcounts[p['id']] -= cnt
|
||||||
|
# we may also be a parent
|
||||||
|
pcounts[gid] = lim - cnt
|
||||||
|
# and add to running total
|
||||||
|
self.newCount += cnt
|
||||||
|
|
||||||
def _resetNew(self):
|
def _resetNew(self):
|
||||||
lim = min(self.queueLimit, self.newCount)
|
self._resetNewCount()
|
||||||
self.newQueue = self.deck.db.all("""
|
self.newGids = self.deck.groups.active()
|
||||||
select id, due from cards where
|
self._newQueue = []
|
||||||
gid in %s and queue = 0 limit %d""" % (self._groupLimit(),
|
|
||||||
lim))
|
|
||||||
self.newQueue.reverse()
|
|
||||||
self._updateNewCardRatio()
|
self._updateNewCardRatio()
|
||||||
|
|
||||||
|
def _fillNew(self):
|
||||||
|
if self._newQueue:
|
||||||
|
return True
|
||||||
|
if not self.newCount:
|
||||||
|
return False
|
||||||
|
while self.newGids:
|
||||||
|
gid = self.newGids[0]
|
||||||
|
lim = min(self.queueLimit, self._groupNewLimit(gid))
|
||||||
|
if lim:
|
||||||
|
# fill the queue with the current gid
|
||||||
|
self._newQueue = self.deck.db.all("""
|
||||||
|
select id, due from cards where gid = ? and queue = 0 limit ?""", gid, lim)
|
||||||
|
if self._newQueue:
|
||||||
|
self._newQueue.reverse()
|
||||||
|
return True
|
||||||
|
# nothing left in the group; move to next
|
||||||
|
self.newGids.pop(0)
|
||||||
|
|
||||||
def _getNewCard(self):
|
def _getNewCard(self):
|
||||||
# We rely on sqlite to return the cards in id order. This may not
|
if not self._fillNew():
|
||||||
# correspond to the 'ord' order. The alternative would be to do
|
return
|
||||||
# something like due = fid*100+ord, but then we have no efficient way
|
(id, due) = self._newQueue.pop()
|
||||||
# of spacing siblings as we'd need to fetch the fid as well.
|
|
||||||
if self.newQueue:
|
|
||||||
(id, due) = self.newQueue.pop()
|
|
||||||
# move any siblings to the end?
|
# move any siblings to the end?
|
||||||
if self.deck.groups.top()['newTodayOrder'] == NEW_TODAY_ORD:
|
conf = self.deck.groups.conf(self.newGids[0])
|
||||||
n = len(self.newQueue)
|
if conf['new']['order'] == NEW_TODAY_ORD:
|
||||||
while self.newQueue and self.newQueue[-1][1] == due:
|
n = len(self._newQueue)
|
||||||
self.newQueue.insert(0, self.newQueue.pop())
|
while self._newQueue and self._newQueue[-1][1] == due:
|
||||||
|
self._newQueue.insert(0, self._newQueue.pop())
|
||||||
n -= 1
|
n -= 1
|
||||||
if not n:
|
if not n:
|
||||||
# we only have one fact in the queue; stop rotating
|
# we only have one fact in the queue; stop rotating
|
||||||
|
@ -282,30 +298,65 @@ gid in %s and queue = 0 limit %d""" % (self._groupLimit(),
|
||||||
elif self.newCardModulus:
|
elif self.newCardModulus:
|
||||||
return self.reps and self.reps % self.newCardModulus == 0
|
return self.reps and self.reps % self.newCardModulus == 0
|
||||||
|
|
||||||
|
def _groupHasNew(self, gid):
|
||||||
|
if not self._groupNewLimit(gid):
|
||||||
|
return False
|
||||||
|
return self.deck.db.scalar(
|
||||||
|
"select 1 from cards where gid = ? and queue = 0 limit 1", gid)
|
||||||
|
|
||||||
|
def _groupNewLimit(self, gid):
|
||||||
|
sel = self.deck.groups.get(gid)
|
||||||
|
lim = -1
|
||||||
|
# for the group and each of its parents
|
||||||
|
for g in [sel] + self.deck.groups.parents(gid):
|
||||||
|
rem = self._groupNewLimitSingle(g)
|
||||||
|
if lim == -1:
|
||||||
|
lim = rem
|
||||||
|
else:
|
||||||
|
lim = min(rem, lim)
|
||||||
|
return lim
|
||||||
|
|
||||||
|
def _groupNewLimitSingle(self, g):
|
||||||
|
# update day if necessary
|
||||||
|
if g['newToday'][0] != self.today:
|
||||||
|
g['newToday'] = [self.today, 0]
|
||||||
|
c = self.deck.groups.conf(g['id'])
|
||||||
|
return max(0, c['new']['perDay'] - g['newToday'][1])
|
||||||
|
|
||||||
# Learning queue
|
# Learning queue
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _resetLrnCount(self):
|
def _resetLrnCount(self):
|
||||||
self._updateStatsDay("lrn")
|
|
||||||
self.lrnCount = self.deck.db.scalar("""
|
self.lrnCount = self.deck.db.scalar("""
|
||||||
select count() from (select id from cards where
|
select count() from (select 1 from cards where
|
||||||
gid in %s and queue = 1 and due < ? limit %d)""" % (
|
gid in %s and queue = 1 and due < ? limit %d)""" % (
|
||||||
self._groupLimit(), self.reportLimit),
|
self._groupLimit(), self.reportLimit),
|
||||||
intTime() + self.deck.groups.top()['collapseTime'])
|
intTime() + self.deck.groups.top()['collapseTime'])
|
||||||
|
|
||||||
def _resetLrn(self):
|
def _resetLrn(self):
|
||||||
self.lrnQueue = self.deck.db.all("""
|
self._resetLrnCount()
|
||||||
|
self._lrnQueue = []
|
||||||
|
|
||||||
|
def _fillLrn(self):
|
||||||
|
if not self.lrnCount:
|
||||||
|
return False
|
||||||
|
if self._lrnQueue:
|
||||||
|
return True
|
||||||
|
self._lrnQueue = self.deck.db.all("""
|
||||||
select due, id from cards where
|
select due, id from cards where
|
||||||
gid in %s and queue = 1 and due < :lim
|
gid in %s and queue = 1 and due < :lim
|
||||||
limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
|
limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
|
||||||
|
# as it arrives sorted by gid first, we need to sort it
|
||||||
|
self._lrnQueue.sort()
|
||||||
|
return self._lrnQueue
|
||||||
|
|
||||||
def _getLrnCard(self, collapse=False):
|
def _getLrnCard(self, collapse=False):
|
||||||
if self.lrnQueue:
|
if self._fillLrn():
|
||||||
cutoff = time.time()
|
cutoff = time.time()
|
||||||
if collapse:
|
if collapse:
|
||||||
cutoff += self.deck.groups.top()['collapseTime']
|
cutoff += self.deck.groups.top()['collapseTime']
|
||||||
if self.lrnQueue[0][0] < cutoff:
|
if self._lrnQueue[0][0] < cutoff:
|
||||||
id = heappop(self.lrnQueue)[1]
|
id = heappop(self._lrnQueue)[1]
|
||||||
self.lrnCount -= 1
|
self.lrnCount -= 1
|
||||||
return id
|
return id
|
||||||
|
|
||||||
|
@ -334,7 +385,7 @@ limit %d""" % (self._groupLimit(), self.reportLimit), lim=self.dayCutoff)
|
||||||
# not collapsed; add some randomness
|
# not collapsed; add some randomness
|
||||||
delay *= random.uniform(1, 1.25)
|
delay *= random.uniform(1, 1.25)
|
||||||
card.due = int(time.time() + delay)
|
card.due = int(time.time() + delay)
|
||||||
heappush(self.lrnQueue, (card.due, card.id))
|
heappush(self._lrnQueue, (card.due, card.id))
|
||||||
# if it's due within the cutoff, increment count
|
# if it's due within the cutoff, increment count
|
||||||
if delay <= self.deck.groups.top()['collapseTime']:
|
if delay <= self.deck.groups.top()['collapseTime']:
|
||||||
self.lrnCount += 1
|
self.lrnCount += 1
|
||||||
|
@ -410,11 +461,22 @@ where queue = 1 and type = 2
|
||||||
%s
|
%s
|
||||||
""" % (intTime(), self.deck.usn(), extra))
|
""" % (intTime(), self.deck.usn(), extra))
|
||||||
|
|
||||||
|
def _groupHasLrn(self, gid):
|
||||||
|
return self.deck.db.scalar(
|
||||||
|
"select 1 from cards where gid = ? and queue = 1 "
|
||||||
|
"and due < ? limit 1",
|
||||||
|
gid, intTime() + self.deck.groups.top()['collapseTime'])
|
||||||
|
|
||||||
# Reviews
|
# Reviews
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
def _groupHasRev(self, gid):
|
||||||
|
return self.deck.db.scalar(
|
||||||
|
"select 1 from cards where gid = ? and queue = 2 "
|
||||||
|
"and due <= ? limit 1",
|
||||||
|
gid, self.today)
|
||||||
|
|
||||||
def _resetRevCount(self):
|
def _resetRevCount(self):
|
||||||
self._updateStatsDay("rev")
|
|
||||||
self.revCount = self.deck.db.scalar("""
|
self.revCount = self.deck.db.scalar("""
|
||||||
select count() from (select id from cards where
|
select count() from (select id from cards where
|
||||||
gid in %s and queue = 2 and due <= :lim limit %d)""" % (
|
gid in %s and queue = 2 and due <= :lim limit %d)""" % (
|
||||||
|
@ -422,25 +484,29 @@ gid in %s and queue = 2 and due <= :lim limit %d)""" % (
|
||||||
lim=self.today)
|
lim=self.today)
|
||||||
|
|
||||||
def _resetRev(self):
|
def _resetRev(self):
|
||||||
self.revQueue = self.deck.db.list("""
|
self._resetRevCount()
|
||||||
|
self._revQueue = []
|
||||||
|
|
||||||
|
def _fillRev(self):
|
||||||
|
if not self.revCount:
|
||||||
|
return False
|
||||||
|
if self._revQueue:
|
||||||
|
return True
|
||||||
|
self._revQueue = self.deck.db.list("""
|
||||||
select id from cards where
|
select id from cards where
|
||||||
gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
|
gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
|
||||||
self._groupLimit(), self._revOrder(), self.queueLimit),
|
self._groupLimit(), self._revOrder(), self.queueLimit),
|
||||||
lim=self.today)
|
lim=self.today)
|
||||||
|
if not self.deck.conf['revOrder']:
|
||||||
r = random.Random()
|
r = random.Random()
|
||||||
r.seed(self.today)
|
r.seed(self.today)
|
||||||
r.shuffle(self.revQueue)
|
r.shuffle(self._revQueue)
|
||||||
|
return True
|
||||||
|
|
||||||
def _getRevCard(self):
|
def _getRevCard(self):
|
||||||
if self._haveRevCards():
|
if self._fillRev():
|
||||||
self.revCount -= 1
|
self.revCount -= 1
|
||||||
return self.revQueue.pop()
|
return self._revQueue.pop()
|
||||||
|
|
||||||
def _haveRevCards(self):
|
|
||||||
if self.revCount:
|
|
||||||
if not self.revQueue:
|
|
||||||
self._resetRev()
|
|
||||||
return self.revQueue
|
|
||||||
|
|
||||||
def _revOrder(self):
|
def _revOrder(self):
|
||||||
if self.deck.conf['revOrder']:
|
if self.deck.conf['revOrder']:
|
||||||
|
@ -472,7 +538,7 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
|
||||||
self.lrnCount += 1
|
self.lrnCount += 1
|
||||||
# leech?
|
# leech?
|
||||||
if not self._checkLeech(card, conf) and conf['relearn']:
|
if not self._checkLeech(card, conf) and conf['relearn']:
|
||||||
heappush(self.lrnQueue, (card.due, card.id))
|
heappush(self._lrnQueue, (card.due, card.id))
|
||||||
|
|
||||||
def _nextLapseIvl(self, card, conf):
|
def _nextLapseIvl(self, card, conf):
|
||||||
return int(card.ivl*conf['mult']) + 1
|
return int(card.ivl*conf['mult']) + 1
|
||||||
|
@ -643,6 +709,8 @@ gid in %s and queue = 2 and due <= :lim %s limit %d""" % (
|
||||||
|
|
||||||
def newTomorrow(self):
|
def newTomorrow(self):
|
||||||
"Number of new cards tomorrow."
|
"Number of new cards tomorrow."
|
||||||
|
print "fixme: rethink newTomorrow() etc"
|
||||||
|
return 1
|
||||||
lim = self.deck.groups.top()['newPerDay']
|
lim = self.deck.groups.top()['newPerDay']
|
||||||
return self.deck.db.scalar(
|
return self.deck.db.scalar(
|
||||||
"select count() from (select id from cards where "
|
"select count() from (select id from cards where "
|
||||||
|
|
|
@ -135,18 +135,26 @@ insert or ignore into deck
|
||||||
values(1,0,0,0,%(v)s,0,0,0,'','{}','','','{}');
|
values(1,0,0,0,%(v)s,0,0,0,'','{}','','','{}');
|
||||||
""" % ({'v':CURRENT_VERSION}))
|
""" % ({'v':CURRENT_VERSION}))
|
||||||
import anki.deck
|
import anki.deck
|
||||||
import anki.groups
|
|
||||||
if setDeckConf:
|
if setDeckConf:
|
||||||
g = anki.groups.defaultTopConf.copy()
|
_addDeckVars(db, *_getDeckVars(db))
|
||||||
|
|
||||||
|
def _getDeckVars(db):
|
||||||
|
import anki.groups
|
||||||
|
g = anki.groups.defaultGroup.copy()
|
||||||
|
for k,v in anki.groups.defaultTopConf.items():
|
||||||
|
g[k] = v
|
||||||
g['id'] = 1
|
g['id'] = 1
|
||||||
g['name'] = _("Default")
|
g['name'] = _("Default")
|
||||||
g['conf'] = 1
|
g['conf'] = 1
|
||||||
g['mod'] = intTime()
|
g['mod'] = intTime()
|
||||||
gc = anki.groups.defaultConf.copy()
|
gc = anki.groups.defaultConf.copy()
|
||||||
gc['id'] = 1
|
gc['id'] = 1
|
||||||
|
return g, gc, anki.deck.defaultConf.copy()
|
||||||
|
|
||||||
|
def _addDeckVars(db, g, gc, c):
|
||||||
db.execute("""
|
db.execute("""
|
||||||
update deck set conf = ?, groups = ?, gconf = ?""",
|
update deck set conf = ?, groups = ?, gconf = ?""",
|
||||||
simplejson.dumps(anki.deck.defaultConf),
|
simplejson.dumps(c),
|
||||||
simplejson.dumps({'1': g}),
|
simplejson.dumps({'1': g}),
|
||||||
simplejson.dumps({'1': gc}))
|
simplejson.dumps({'1': gc}))
|
||||||
|
|
||||||
|
@ -356,14 +364,7 @@ insert or replace into deck select id, cast(created as int), :t,
|
||||||
:t, 99, 0, 0, cast(lastSync as int),
|
:t, 99, 0, 0, cast(lastSync as int),
|
||||||
"", "", "", "", "" from decks""", t=intTime())
|
"", "", "", "", "" from decks""", t=intTime())
|
||||||
# prepare a group to store the old deck options
|
# prepare a group to store the old deck options
|
||||||
import anki.groups
|
g, gc, conf = _getDeckVars(db)
|
||||||
g = anki.groups.defaultTopConf.copy()
|
|
||||||
g['id'] = 1
|
|
||||||
g['name'] = _("Default")
|
|
||||||
g['conf'] = 1
|
|
||||||
g['mod'] = intTime()
|
|
||||||
# and deck conf
|
|
||||||
conf = anki.deck.defaultConf.copy()
|
|
||||||
# delete old selective study settings, which we can't auto-upgrade easily
|
# delete old selective study settings, which we can't auto-upgrade easily
|
||||||
keys = ("newActive", "newInactive", "revActive", "revInactive")
|
keys = ("newActive", "newInactive", "revActive", "revInactive")
|
||||||
for k in keys:
|
for k in keys:
|
||||||
|
@ -373,7 +374,6 @@ insert or replace into deck select id, cast(created as int), :t,
|
||||||
g['newPerDay'] = db.scalar("select newCardsPerDay from decks")
|
g['newPerDay'] = db.scalar("select newCardsPerDay from decks")
|
||||||
g['repLim'] = db.scalar("select sessionRepLimit from decks")
|
g['repLim'] = db.scalar("select sessionRepLimit from decks")
|
||||||
g['timeLim'] = db.scalar("select sessionTimeLimit from decks")
|
g['timeLim'] = db.scalar("select sessionTimeLimit from decks")
|
||||||
|
|
||||||
# this needs to be placed in the model later on
|
# this needs to be placed in the model later on
|
||||||
conf['oldNewOrder'] = db.scalar("select newCardOrder from decks")
|
conf['oldNewOrder'] = db.scalar("select newCardOrder from decks")
|
||||||
# no reverse option anymore
|
# no reverse option anymore
|
||||||
|
@ -385,10 +385,7 @@ insert or replace into deck select id, cast(created as int), :t,
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
conf[k] = v
|
conf[k] = v
|
||||||
db.execute("update deck set conf=:c,groups=:g,gconf=:gc",
|
_addDeckVars(db, g, gc, conf)
|
||||||
c=simplejson.dumps(conf),
|
|
||||||
g=simplejson.dumps({'1': g}),
|
|
||||||
gc=simplejson.dumps({'1': anki.groups.defaultConf}))
|
|
||||||
# clean up
|
# clean up
|
||||||
db.execute("drop table decks")
|
db.execute("drop table decks")
|
||||||
db.execute("drop table deckVars")
|
db.execute("drop table deckVars")
|
||||||
|
|
|
@ -48,6 +48,36 @@ def test_new():
|
||||||
assert(stripHTML(c.q()) == qs[n])
|
assert(stripHTML(c.q()) == qs[n])
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
|
def test_newLimits():
|
||||||
|
d = getEmptyDeck()
|
||||||
|
# add some facts
|
||||||
|
g2 = d.groups.id("Default::foo")
|
||||||
|
for i in range(30):
|
||||||
|
f = d.newFact()
|
||||||
|
f['Front'] = str(i)
|
||||||
|
if i > 4:
|
||||||
|
f.gid = g2
|
||||||
|
d.addFact(f)
|
||||||
|
# give the child group a different configuration
|
||||||
|
c2 = d.groups.confId("new conf")
|
||||||
|
d.groups.setConf(d.groups.get(g2), c2)
|
||||||
|
d.reset()
|
||||||
|
# both confs have defaulted to a limit of 20
|
||||||
|
assert d.sched.newCount == 20
|
||||||
|
# first card we get comes from parent
|
||||||
|
c = d.sched.getCard()
|
||||||
|
assert c.gid == 1
|
||||||
|
# limit the parent to 10 cards, meaning we get 10 in total
|
||||||
|
conf1 = d.groups.conf(1)
|
||||||
|
conf1['new']['perDay'] = 10
|
||||||
|
d.reset()
|
||||||
|
assert d.sched.newCount == 10
|
||||||
|
# if we limit child to 4, we should get 9
|
||||||
|
conf2 = d.groups.conf(g2)
|
||||||
|
conf2['new']['perDay'] = 4
|
||||||
|
d.reset()
|
||||||
|
assert d.sched.newCount == 9
|
||||||
|
|
||||||
def test_newOrder():
|
def test_newOrder():
|
||||||
d = getEmptyDeck()
|
d = getEmptyDeck()
|
||||||
m = d.models.current()
|
m = d.models.current()
|
||||||
|
@ -69,7 +99,7 @@ def test_newOrder():
|
||||||
d.conf['newPerDay'] = 100
|
d.conf['newPerDay'] = 100
|
||||||
d.reset()
|
d.reset()
|
||||||
# cards should be sorted by id
|
# cards should be sorted by id
|
||||||
assert d.sched.newQueue == list(reversed(sorted(d.sched.newQueue)))
|
assert d.sched._newQueue == list(reversed(sorted(d.sched._newQueue)))
|
||||||
|
|
||||||
def test_newBoxes():
|
def test_newBoxes():
|
||||||
d = getEmptyDeck()
|
d = getEmptyDeck()
|
||||||
|
@ -725,6 +755,30 @@ def test_groupCounts():
|
||||||
d.sched.groupCounts()
|
d.sched.groupCounts()
|
||||||
d.sched.groupCountTree()
|
d.sched.groupCountTree()
|
||||||
|
|
||||||
|
def test_groupFlow():
|
||||||
|
d = getEmptyDeck()
|
||||||
|
# add a fact with default group
|
||||||
|
f = d.newFact()
|
||||||
|
f['Front'] = u"one"
|
||||||
|
d.addFact(f)
|
||||||
|
# and one that's a child
|
||||||
|
f = d.newFact()
|
||||||
|
f['Front'] = u"two"
|
||||||
|
default1 = f.gid = d.groups.id("Default::2")
|
||||||
|
d.addFact(f)
|
||||||
|
# and another that's higher up
|
||||||
|
f = d.newFact()
|
||||||
|
f['Front'] = u"three"
|
||||||
|
default1 = f.gid = d.groups.id("Default::1")
|
||||||
|
d.addFact(f)
|
||||||
|
# should get top level one first, then ::1, then ::2
|
||||||
|
d.reset()
|
||||||
|
assert d.sched.counts() == (3,0,0)
|
||||||
|
for i in "one", "three", "two":
|
||||||
|
c = d.sched.getCard()
|
||||||
|
assert c.fact()['Front'] == i
|
||||||
|
d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
def test_reorder():
|
def test_reorder():
|
||||||
d = getEmptyDeck()
|
d = getEmptyDeck()
|
||||||
# add a fact with default group
|
# add a fact with default group
|
||||||
|
|
Loading…
Reference in a new issue