mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 15:02:21 -04:00
remove the stats table
The stats table was how the early non-SQL versions of Anki kept track of statistics, before there was a revision log. It is being removed because: - it's not possible to show the statistics for a subset of the deck - it can't meaningfully be copied on import/export - it makes it harder to implement sync merging Implications: - graphs and deck stats roughly 1.5-3x longer than before, but we'll have the ability to generate stats for subsections of the deck, and it's not time critical code - people who've been using anki since the very early days may notice a drop in statistics, as early repetitions were recorded in the stats table but the revlog didn't exist at that point. - due bugs in old syncs and imports/exports, the stats and revlog may not match numbers exactly To remove it, the following changes have been made: - the graphs and deck stats now generate their data entirely from the revlog - there are no stats to keep track of how many cards we've answered, so we pull that information from the revlog in reset() - we remove _globalStats and _dailyStats from the deck - we check if a day rollover has occurred using failedCutoff instead - we remove the getStats() routine - the ETA code is currently disabled - timeboxing routines use repsToday instead of stats - remove stats delete from export - remove stats table and index in upgrade - remove stats syncing and globalStats refresh pre-sync - remove stats count check in fullSync check, which was redundant anyway - update unit tests Also: - newCountToday -> newCount, to bring it in line with revCount&failedCount which also reflect the currently due count - newCount -> newAvail - timeboxing routines renamed since the old names were confusingly similar to refreshSession() which does something different Todo: - update newSeenToday & repsToday when answering a card - reimplement eta
This commit is contained in:
parent
9ce60d5e3a
commit
855de47ffe
7 changed files with 54 additions and 359 deletions
88
anki/deck.py
88
anki/deck.py
|
@ -13,7 +13,6 @@ from anki.utils import parseTags, tidyHTML, genID, ids2str, hexifyID, \
|
||||||
canonifyTags, joinTags, addTags, checksum, fieldChecksum
|
canonifyTags, joinTags, addTags, checksum, fieldChecksum
|
||||||
from anki.history import CardHistoryEntry
|
from anki.history import CardHistoryEntry
|
||||||
from anki.models import Model, CardModel, formatQA
|
from anki.models import Model, CardModel, formatQA
|
||||||
from anki.stats import dailyStats, globalStats, genToday
|
|
||||||
from anki.fonts import toPlatformFont
|
from anki.fonts import toPlatformFont
|
||||||
from anki.tags import initTagTables, tagIds
|
from anki.tags import initTagTables, tagIds
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
@ -26,7 +25,7 @@ from anki.upgrade import upgradeSchema, updateIndices, upgradeDeck, DECK_VERSION
|
||||||
import anki.latex # sets up hook
|
import anki.latex # sets up hook
|
||||||
|
|
||||||
# ensure all the DB metadata in other files is loaded before proceeding
|
# ensure all the DB metadata in other files is loaded before proceeding
|
||||||
import anki.models, anki.facts, anki.cards, anki.stats
|
import anki.models, anki.facts, anki.cards
|
||||||
import anki.history, anki.media
|
import anki.history, anki.media
|
||||||
|
|
||||||
# rest
|
# rest
|
||||||
|
@ -206,6 +205,13 @@ class Deck(object):
|
||||||
# global counts
|
# global counts
|
||||||
self.cardCount = self.s.scalar("select count(*) from cards")
|
self.cardCount = self.s.scalar("select count(*) from cards")
|
||||||
self.factCount = self.s.scalar("select count(*) from facts")
|
self.factCount = self.s.scalar("select count(*) from facts")
|
||||||
|
# day counts
|
||||||
|
(self.repsToday, self.newSeenToday) = self.s.first("""
|
||||||
|
select count(), sum(case when reps = 1 then 1 else 0 end) from reviewHistory
|
||||||
|
where time > :t""", t=self.failedCutoff-86400)
|
||||||
|
self.newSeenToday = self.newSeenToday or 0
|
||||||
|
print "newSeenToday in answer(), reset called twice"
|
||||||
|
print "newSeenToday needs to account for drill mode too."
|
||||||
# due counts
|
# due counts
|
||||||
self.rebuildFailedCount()
|
self.rebuildFailedCount()
|
||||||
self.rebuildRevCount()
|
self.rebuildRevCount()
|
||||||
|
@ -251,7 +257,7 @@ class Deck(object):
|
||||||
"and combinedDue < :lim"), lim=self.dueCutoff)
|
"and combinedDue < :lim"), lim=self.dueCutoff)
|
||||||
|
|
||||||
def _rebuildNewCount(self):
|
def _rebuildNewCount(self):
|
||||||
self.newCount = self.s.scalar(
|
self.newAvail = self.s.scalar(
|
||||||
self.cardLimit(
|
self.cardLimit(
|
||||||
"newActive", "newInactive",
|
"newActive", "newInactive",
|
||||||
"select count(*) from cards c where type = 2 "
|
"select count(*) from cards c where type = 2 "
|
||||||
|
@ -260,9 +266,9 @@ class Deck(object):
|
||||||
self.spacedCards = []
|
self.spacedCards = []
|
||||||
|
|
||||||
def _updateNewCountToday(self):
|
def _updateNewCountToday(self):
|
||||||
self.newCountToday = max(min(
|
self.newCount = max(min(
|
||||||
self.newCount, self.newCardsPerDay -
|
self.newAvail, self.newCardsPerDay -
|
||||||
self.newCardsDoneToday()), 0)
|
self.newSeenToday), 0)
|
||||||
|
|
||||||
def _fillFailedQueue(self):
|
def _fillFailedQueue(self):
|
||||||
if self.failedSoonCount and not self.failedQueue:
|
if self.failedSoonCount and not self.failedQueue:
|
||||||
|
@ -285,7 +291,7 @@ limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dueCutoff)
|
||||||
self.revQueue.reverse()
|
self.revQueue.reverse()
|
||||||
|
|
||||||
def _fillNewQueue(self):
|
def _fillNewQueue(self):
|
||||||
if self.newCountToday and not self.newQueue and not self.spacedCards:
|
if self.newCount and not self.newQueue and not self.spacedCards:
|
||||||
self.newQueue = self.s.all(
|
self.newQueue = self.s.all(
|
||||||
self.cardLimit(
|
self.cardLimit(
|
||||||
"newActive", "newInactive", """
|
"newActive", "newInactive", """
|
||||||
|
@ -355,7 +361,7 @@ produce the problem.
|
||||||
Counts %d %d %d
|
Counts %d %d %d
|
||||||
Queue %d %d %d
|
Queue %d %d %d
|
||||||
Card info: %d %d %d
|
Card info: %d %d %d
|
||||||
New type: %s""" % (self.failedSoonCount, self.revCount, self.newCountToday,
|
New type: %s""" % (self.failedSoonCount, self.revCount, self.newCount,
|
||||||
len(self.failedQueue), len(self.revQueue),
|
len(self.failedQueue), len(self.revQueue),
|
||||||
len(self.newQueue),
|
len(self.newQueue),
|
||||||
card.reps, card.successive, oldSuc, `newType`))
|
card.reps, card.successive, oldSuc, `newType`))
|
||||||
|
@ -443,9 +449,6 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
|
||||||
self.dueCutoff = time.time()
|
self.dueCutoff = time.time()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
# setup global/daily stats
|
|
||||||
self._globalStats = globalStats(self)
|
|
||||||
self._dailyStats = dailyStats(self)
|
|
||||||
# recheck counts
|
# recheck counts
|
||||||
self.rebuildCounts()
|
self.rebuildCounts()
|
||||||
# empty queues; will be refilled by getCard()
|
# empty queues; will be refilled by getCard()
|
||||||
|
@ -455,9 +458,9 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
|
||||||
self.spacedFacts = {}
|
self.spacedFacts = {}
|
||||||
# determine new card distribution
|
# determine new card distribution
|
||||||
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
|
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
|
||||||
if self.newCountToday:
|
if self.newCount:
|
||||||
self.newCardModulus = (
|
self.newCardModulus = (
|
||||||
(self.newCountToday + self.revCount) / self.newCountToday)
|
(self.newCount + self.revCount) / self.newCount)
|
||||||
# if there are cards to review, ensure modulo >= 2
|
# if there are cards to review, ensure modulo >= 2
|
||||||
if self.revCount:
|
if self.revCount:
|
||||||
self.newCardModulus = max(2, self.newCardModulus)
|
self.newCardModulus = max(2, self.newCardModulus)
|
||||||
|
@ -474,7 +477,7 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
|
||||||
|
|
||||||
def checkDay(self):
|
def checkDay(self):
|
||||||
# check if the day has rolled over
|
# check if the day has rolled over
|
||||||
if genToday(self) != self._dailyStats.day:
|
if time.time() > self.failedCutoff:
|
||||||
self.updateCutoff()
|
self.updateCutoff()
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
@ -531,7 +534,7 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
|
||||||
self.scheduler = "learnMore"
|
self.scheduler = "learnMore"
|
||||||
|
|
||||||
def _rebuildLearnMoreCount(self):
|
def _rebuildLearnMoreCount(self):
|
||||||
self.newCount = self.s.scalar(
|
self.newAvail = self.s.scalar(
|
||||||
self.cardLimit(
|
self.cardLimit(
|
||||||
"newActive", "newInactive",
|
"newActive", "newInactive",
|
||||||
"select count(*) from cards c where type = 2 "
|
"select count(*) from cards c where type = 2 "
|
||||||
|
@ -539,7 +542,7 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
|
||||||
self.spacedCards = []
|
self.spacedCards = []
|
||||||
|
|
||||||
def _updateLearnMoreCountToday(self):
|
def _updateLearnMoreCountToday(self):
|
||||||
self.newCountToday = self.newCount
|
self.newCount = self.newAvail
|
||||||
|
|
||||||
# Cramming
|
# Cramming
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -611,8 +614,8 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
|
||||||
self.failedCramQueue.pop()
|
self.failedCramQueue.pop()
|
||||||
|
|
||||||
def _rebuildCramNewCount(self):
|
def _rebuildCramNewCount(self):
|
||||||
|
self.newAvail = 0
|
||||||
self.newCount = 0
|
self.newCount = 0
|
||||||
self.newCountToday = 0
|
|
||||||
|
|
||||||
def _cramCardLimit(self, active, inactive, sql):
|
def _cramCardLimit(self, active, inactive, sql):
|
||||||
# inactive is (currently) ignored
|
# inactive is (currently) ignored
|
||||||
|
@ -683,7 +686,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
|
||||||
if self.revNoSpaced():
|
if self.revNoSpaced():
|
||||||
return self.revQueue[-1][0]
|
return self.revQueue[-1][0]
|
||||||
# new cards left?
|
# new cards left?
|
||||||
if self.newCountToday:
|
if self.newCount:
|
||||||
id = self.getNewCard()
|
id = self.getNewCard()
|
||||||
if id:
|
if id:
|
||||||
return id
|
return id
|
||||||
|
@ -706,7 +709,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
|
||||||
|
|
||||||
def _timeForNewCard(self):
|
def _timeForNewCard(self):
|
||||||
"True if it's time to display a new card when distributing."
|
"True if it's time to display a new card when distributing."
|
||||||
if not self.newCountToday:
|
if not self.newCount:
|
||||||
return False
|
return False
|
||||||
if self.newCardSpacing == NEW_CARDS_LAST:
|
if self.newCardSpacing == NEW_CARDS_LAST:
|
||||||
return False
|
return False
|
||||||
|
@ -797,7 +800,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
|
||||||
elif oldQueue == 1:
|
elif oldQueue == 1:
|
||||||
self.revCount -= 1
|
self.revCount -= 1
|
||||||
else:
|
else:
|
||||||
self.newCount -= 1
|
self.newAvail -= 1
|
||||||
# card stats
|
# card stats
|
||||||
anki.cards.Card.updateStats(card, ease, oldState)
|
anki.cards.Card.updateStats(card, ease, oldState)
|
||||||
# update type & ensure past cutoff
|
# update type & ensure past cutoff
|
||||||
|
@ -811,9 +814,6 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
|
||||||
# save
|
# save
|
||||||
card.combinedDue = card.due
|
card.combinedDue = card.due
|
||||||
card.toDB(self.s)
|
card.toDB(self.s)
|
||||||
# global/daily stats
|
|
||||||
anki.stats.updateAllStats(self.s, self._globalStats, self._dailyStats,
|
|
||||||
card, ease, oldState)
|
|
||||||
# review history
|
# review history
|
||||||
entry = CardHistoryEntry(card, ease, lastDelay)
|
entry = CardHistoryEntry(card, ease, lastDelay)
|
||||||
entry.writeSQL(self.s)
|
entry.writeSQL(self.s)
|
||||||
|
@ -1187,13 +1187,6 @@ where type between -3 and -1 and id in %s""" %
|
||||||
select 1 from cards where combinedDue < :now
|
select 1 from cards where combinedDue < :now
|
||||||
and type between 0 and 1 limit 1""", now=self.dueCutoff)
|
and type between 0 and 1 limit 1""", now=self.dueCutoff)
|
||||||
|
|
||||||
def newCardsDoneToday(self):
|
|
||||||
return (self._dailyStats.newEase0 +
|
|
||||||
self._dailyStats.newEase1 +
|
|
||||||
self._dailyStats.newEase2 +
|
|
||||||
self._dailyStats.newEase3 +
|
|
||||||
self._dailyStats.newEase4)
|
|
||||||
|
|
||||||
def spacedCardCount(self):
|
def spacedCardCount(self):
|
||||||
"Number of spaced cards."
|
"Number of spaced cards."
|
||||||
return self.s.scalar("""
|
return self.s.scalar("""
|
||||||
|
@ -1251,22 +1244,9 @@ combinedDue > :now and due < :now""", now=time.time())
|
||||||
# Stats
|
# Stats
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def getStats(self, short=False):
|
|
||||||
"Return some commonly needed stats."
|
|
||||||
stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats)
|
|
||||||
# add scheduling related stats
|
|
||||||
stats['new'] = self.newCountToday
|
|
||||||
stats['failed'] = self.failedSoonCount
|
|
||||||
stats['rev'] = self.revCount
|
|
||||||
if stats['dAverageTime']:
|
|
||||||
stats['timeLeft'] = anki.utils.fmtTimeSpan(
|
|
||||||
self.getETA(stats), pad=0, point=1, short=short)
|
|
||||||
else:
|
|
||||||
stats['timeLeft'] = _("Unknown")
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def getETA(self, stats):
|
def getETA(self, stats):
|
||||||
# rev + new cards first, account for failures
|
# rev + new cards first, account for failures
|
||||||
|
import traceback; traceback.print_stack()
|
||||||
count = stats['rev'] + stats['new']
|
count = stats['rev'] + stats['new']
|
||||||
count *= 1 + stats['gYoungNo%'] / 100.0
|
count *= 1 + stats['gYoungNo%'] / 100.0
|
||||||
left = count * stats['dAverageTime']
|
left = count * stats['dAverageTime']
|
||||||
|
@ -1377,7 +1357,8 @@ where factId = :fid and cardModelId = :cmid""",
|
||||||
fact.created+0.0001*cardModel.ordinal)
|
fact.created+0.0001*cardModel.ordinal)
|
||||||
self.updateCardTags([card.id])
|
self.updateCardTags([card.id])
|
||||||
self.cardCount += 1
|
self.cardCount += 1
|
||||||
self.newCount += 1
|
raise Exception("incorrect; not checking selective study")
|
||||||
|
self.newAvail += 1
|
||||||
ids.append(card.id)
|
ids.append(card.id)
|
||||||
|
|
||||||
if ids:
|
if ids:
|
||||||
|
@ -2794,18 +2775,21 @@ select id from facts where spaceUntil like :_ff_%d escape '\\'""" % c
|
||||||
assert '\\' not in n
|
assert '\\' not in n
|
||||||
return n
|
return n
|
||||||
|
|
||||||
# Session handling
|
# Timeboxing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def startSession(self):
|
def startTimebox(self):
|
||||||
self.lastSessionStart = self.sessionStartTime
|
self.lastSessionStart = self.sessionStartTime
|
||||||
self.sessionStartTime = time.time()
|
self.sessionStartTime = time.time()
|
||||||
self.sessionStartReps = self.getStats()['dTotal']
|
self.sessionStartReps = self.repsToday
|
||||||
|
|
||||||
def stopSession(self):
|
def stopTimebox(self):
|
||||||
self.sessionStartTime = 0
|
self.sessionStartTime = 0
|
||||||
|
|
||||||
def sessionLimitReached(self):
|
def timeboxStarted(self):
|
||||||
|
return self.sessionStartTime
|
||||||
|
|
||||||
|
def timeboxReached(self):
|
||||||
if not self.sessionStartTime:
|
if not self.sessionStartTime:
|
||||||
# not started
|
# not started
|
||||||
return False
|
return False
|
||||||
|
@ -2813,7 +2797,7 @@ select id from facts where spaceUntil like :_ff_%d escape '\\'""" % c
|
||||||
(self.sessionStartTime + self.sessionTimeLimit)):
|
(self.sessionStartTime + self.sessionTimeLimit)):
|
||||||
return True
|
return True
|
||||||
if (self.sessionRepLimit and self.sessionRepLimit <=
|
if (self.sessionRepLimit and self.sessionRepLimit <=
|
||||||
self.getStats()['dTotal'] - self.sessionStartReps):
|
self.repsToday - self.sessionStartReps):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -3606,8 +3590,6 @@ class DeckStorage(object):
|
||||||
raise e
|
raise e
|
||||||
if not rebuild:
|
if not rebuild:
|
||||||
# minimal startup
|
# minimal startup
|
||||||
deck._globalStats = globalStats(deck)
|
|
||||||
deck._dailyStats = dailyStats(deck)
|
|
||||||
return deck
|
return deck
|
||||||
oldMod = deck.modified
|
oldMod = deck.modified
|
||||||
# fix a bug with current model being unset
|
# fix a bug with current model being unset
|
||||||
|
|
|
@ -126,8 +126,6 @@ relativeDelay = 2,
|
||||||
combinedDue = created,
|
combinedDue = created,
|
||||||
modified = :now
|
modified = :now
|
||||||
""", now=time.time())
|
""", now=time.time())
|
||||||
self.newDeck.s.statement("""
|
|
||||||
delete from stats""")
|
|
||||||
# media
|
# media
|
||||||
if self.includeMedia:
|
if self.includeMedia:
|
||||||
server.deck.mediaPrefix = ""
|
server.deck.mediaPrefix = ""
|
||||||
|
|
|
@ -2,12 +2,9 @@
|
||||||
# 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 os, sys, time
|
import os, sys, time, datetime
|
||||||
import anki.stats
|
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
#colours for graphs
|
#colours for graphs
|
||||||
dueYoungC = "#ffb380"
|
dueYoungC = "#ffb380"
|
||||||
dueMatureC = "#ff5555"
|
dueMatureC = "#ff5555"
|
||||||
|
@ -100,16 +97,17 @@ from cards c where relativeDelay = 1 and type >= 0 and interval > 21"""
|
||||||
|
|
||||||
dayReps = self.getDayReps()
|
dayReps = self.getDayReps()
|
||||||
|
|
||||||
todaydt = self.deck._dailyStats.day
|
todaydt = datetime.datetime.utcfromtimestamp(
|
||||||
|
time.time() - self.deck.utcOffset).date()
|
||||||
for dest, source in [("dayRepsNew", "combinedNewReps"),
|
for dest, source in [("dayRepsNew", "combinedNewReps"),
|
||||||
("dayRepsYoung", "combinedYoungReps"),
|
("dayRepsYoung", "combinedYoungReps"),
|
||||||
("dayRepsMature", "matureReps")]:
|
("dayRepsMature", "matureReps")]:
|
||||||
self.stats[dest] = dict(
|
self.stats[dest] = dict(
|
||||||
map(lambda dr: (-(todaydt -datetime.date(
|
map(lambda dr: (-(todaydt - datetime.date(
|
||||||
*(int(x)for x in dr["day"].split("-")))).days, dr[source]), dayReps))
|
*(int(x)for x in dr["day"].split("-")))).days, dr[source]), dayReps))
|
||||||
|
|
||||||
self.stats['dayTimes'] = dict(
|
self.stats['dayTimes'] = dict(
|
||||||
map(lambda dr: (-(todaydt -datetime.date(
|
map(lambda dr: (-(todaydt - datetime.date(
|
||||||
*(int(x)for x in dr["day"].split("-")))).days, dr["reviewTime"]/60.0), dayReps))
|
*(int(x)for x in dr["day"].split("-")))).days, dr["reviewTime"]/60.0), dayReps))
|
||||||
|
|
||||||
def getDayReps(self):
|
def getDayReps(self):
|
||||||
|
|
239
anki/stats.py
239
anki/stats.py
|
@ -2,246 +2,13 @@
|
||||||
# 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
|
||||||
|
|
||||||
# we track statistics over the life of the deck, and per-day
|
import time, sys, os, datetime
|
||||||
STATS_LIFE = 0
|
|
||||||
STATS_DAY = 1
|
|
||||||
|
|
||||||
import unicodedata, time, sys, os, datetime
|
|
||||||
import anki, anki.utils
|
import anki, anki.utils
|
||||||
from datetime import date
|
|
||||||
from anki.db import *
|
from anki.db import *
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
from anki.utils import canonifyTags, ids2str
|
from anki.utils import canonifyTags, ids2str
|
||||||
from anki.hooks import runFilter
|
from anki.hooks import runFilter
|
||||||
|
|
||||||
# Tracking stats on the DB
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
statsTable = Table(
|
|
||||||
'stats', metadata,
|
|
||||||
Column('id', Integer, primary_key=True),
|
|
||||||
Column('type', Integer, nullable=False),
|
|
||||||
Column('day', Date, nullable=False),
|
|
||||||
Column('reps', Integer, nullable=False, default=0),
|
|
||||||
Column('averageTime', Float, nullable=False, default=0),
|
|
||||||
Column('reviewTime', Float, nullable=False, default=0),
|
|
||||||
# next two columns no longer used
|
|
||||||
Column('distractedTime', Float, nullable=False, default=0),
|
|
||||||
Column('distractedReps', Integer, nullable=False, default=0),
|
|
||||||
Column('newEase0', Integer, nullable=False, default=0),
|
|
||||||
Column('newEase1', Integer, nullable=False, default=0),
|
|
||||||
Column('newEase2', Integer, nullable=False, default=0),
|
|
||||||
Column('newEase3', Integer, nullable=False, default=0),
|
|
||||||
Column('newEase4', Integer, nullable=False, default=0),
|
|
||||||
Column('youngEase0', Integer, nullable=False, default=0),
|
|
||||||
Column('youngEase1', Integer, nullable=False, default=0),
|
|
||||||
Column('youngEase2', Integer, nullable=False, default=0),
|
|
||||||
Column('youngEase3', Integer, nullable=False, default=0),
|
|
||||||
Column('youngEase4', Integer, nullable=False, default=0),
|
|
||||||
Column('matureEase0', Integer, nullable=False, default=0),
|
|
||||||
Column('matureEase1', Integer, nullable=False, default=0),
|
|
||||||
Column('matureEase2', Integer, nullable=False, default=0),
|
|
||||||
Column('matureEase3', Integer, nullable=False, default=0),
|
|
||||||
Column('matureEase4', Integer, nullable=False, default=0))
|
|
||||||
|
|
||||||
class Stats(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.day = None
|
|
||||||
self.reps = 0
|
|
||||||
self.averageTime = 0
|
|
||||||
self.reviewTime = 0
|
|
||||||
self.distractedTime = 0
|
|
||||||
self.distractedReps = 0
|
|
||||||
self.newEase0 = 0
|
|
||||||
self.newEase1 = 0
|
|
||||||
self.newEase2 = 0
|
|
||||||
self.newEase3 = 0
|
|
||||||
self.newEase4 = 0
|
|
||||||
self.youngEase0 = 0
|
|
||||||
self.youngEase1 = 0
|
|
||||||
self.youngEase2 = 0
|
|
||||||
self.youngEase3 = 0
|
|
||||||
self.youngEase4 = 0
|
|
||||||
self.matureEase0 = 0
|
|
||||||
self.matureEase1 = 0
|
|
||||||
self.matureEase2 = 0
|
|
||||||
self.matureEase3 = 0
|
|
||||||
self.matureEase4 = 0
|
|
||||||
|
|
||||||
def fromDB(self, s, id):
|
|
||||||
r = s.first("select * from stats where id = :id", id=id)
|
|
||||||
(self.id,
|
|
||||||
self.type,
|
|
||||||
self.day,
|
|
||||||
self.reps,
|
|
||||||
self.averageTime,
|
|
||||||
self.reviewTime,
|
|
||||||
self.distractedTime,
|
|
||||||
self.distractedReps,
|
|
||||||
self.newEase0,
|
|
||||||
self.newEase1,
|
|
||||||
self.newEase2,
|
|
||||||
self.newEase3,
|
|
||||||
self.newEase4,
|
|
||||||
self.youngEase0,
|
|
||||||
self.youngEase1,
|
|
||||||
self.youngEase2,
|
|
||||||
self.youngEase3,
|
|
||||||
self.youngEase4,
|
|
||||||
self.matureEase0,
|
|
||||||
self.matureEase1,
|
|
||||||
self.matureEase2,
|
|
||||||
self.matureEase3,
|
|
||||||
self.matureEase4) = r
|
|
||||||
self.day = datetime.date(*[int(i) for i in self.day.split("-")])
|
|
||||||
|
|
||||||
def create(self, s, type, day):
|
|
||||||
self.type = type
|
|
||||||
self.day = day
|
|
||||||
s.execute("""insert into stats
|
|
||||||
(type, day, reps, averageTime, reviewTime, distractedTime, distractedReps,
|
|
||||||
newEase0, newEase1, newEase2, newEase3, newEase4, youngEase0, youngEase1,
|
|
||||||
youngEase2, youngEase3, youngEase4, matureEase0, matureEase1, matureEase2,
|
|
||||||
matureEase3, matureEase4) values (:type, :day, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)""", self.__dict__)
|
|
||||||
self.id = s.scalar(
|
|
||||||
"select id from stats where type = :type and day = :day",
|
|
||||||
type=type, day=day)
|
|
||||||
|
|
||||||
def toDB(self, s):
|
|
||||||
assert self.id
|
|
||||||
s.execute("""update stats set
|
|
||||||
type=:type,
|
|
||||||
day=:day,
|
|
||||||
reps=:reps,
|
|
||||||
averageTime=:averageTime,
|
|
||||||
reviewTime=:reviewTime,
|
|
||||||
newEase0=:newEase0,
|
|
||||||
newEase1=:newEase1,
|
|
||||||
newEase2=:newEase2,
|
|
||||||
newEase3=:newEase3,
|
|
||||||
newEase4=:newEase4,
|
|
||||||
youngEase0=:youngEase0,
|
|
||||||
youngEase1=:youngEase1,
|
|
||||||
youngEase2=:youngEase2,
|
|
||||||
youngEase3=:youngEase3,
|
|
||||||
youngEase4=:youngEase4,
|
|
||||||
matureEase0=:matureEase0,
|
|
||||||
matureEase1=:matureEase1,
|
|
||||||
matureEase2=:matureEase2,
|
|
||||||
matureEase3=:matureEase3,
|
|
||||||
matureEase4=:matureEase4
|
|
||||||
where id = :id""", self.__dict__)
|
|
||||||
|
|
||||||
mapper(Stats, statsTable)
|
|
||||||
|
|
||||||
def genToday(deck):
|
|
||||||
return datetime.datetime.utcfromtimestamp(
|
|
||||||
time.time() - deck.utcOffset).date()
|
|
||||||
|
|
||||||
def updateAllStats(s, gs, ds, card, ease, oldState):
|
|
||||||
"Update global and daily statistics."
|
|
||||||
updateStats(s, gs, card, ease, oldState)
|
|
||||||
updateStats(s, ds, card, ease, oldState)
|
|
||||||
|
|
||||||
def updateStats(s, stats, card, ease, oldState):
|
|
||||||
stats.reps += 1
|
|
||||||
delay = card.totalTime()
|
|
||||||
if delay >= 60:
|
|
||||||
stats.reviewTime += 60
|
|
||||||
else:
|
|
||||||
stats.reviewTime += delay
|
|
||||||
stats.averageTime = (
|
|
||||||
stats.reviewTime / float(stats.reps))
|
|
||||||
# update eases
|
|
||||||
attr = oldState + "Ease%d" % ease
|
|
||||||
setattr(stats, attr, getattr(stats, attr) + 1)
|
|
||||||
stats.toDB(s)
|
|
||||||
|
|
||||||
def globalStats(deck):
|
|
||||||
s = deck.s
|
|
||||||
type = STATS_LIFE
|
|
||||||
today = genToday(deck)
|
|
||||||
id = s.scalar("select id from stats where type = :type",
|
|
||||||
type=type)
|
|
||||||
stats = Stats()
|
|
||||||
if id:
|
|
||||||
stats.fromDB(s, id)
|
|
||||||
return stats
|
|
||||||
else:
|
|
||||||
stats.create(s, type, today)
|
|
||||||
stats.type = type
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def dailyStats(deck):
|
|
||||||
s = deck.s
|
|
||||||
type = STATS_DAY
|
|
||||||
today = genToday(deck)
|
|
||||||
id = s.scalar("select id from stats where type = :type and day = :day",
|
|
||||||
type=type, day=today)
|
|
||||||
stats = Stats()
|
|
||||||
if id:
|
|
||||||
stats.fromDB(s, id)
|
|
||||||
return stats
|
|
||||||
else:
|
|
||||||
stats.create(s, type, today)
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def summarizeStats(stats, pre=""):
|
|
||||||
"Generate percentages and total counts for STATS. Optionally prefix."
|
|
||||||
cardTypes = ("new", "young", "mature")
|
|
||||||
h = {}
|
|
||||||
# total counts
|
|
||||||
###############
|
|
||||||
for type in cardTypes:
|
|
||||||
# total yes/no for type, eg. gNewYes
|
|
||||||
h[pre + type.capitalize() + "No"] = (getattr(stats, type + "Ease0") +
|
|
||||||
getattr(stats, type + "Ease1"))
|
|
||||||
h[pre + type.capitalize() + "Yes"] = (getattr(stats, type + "Ease2") +
|
|
||||||
getattr(stats, type + "Ease3") +
|
|
||||||
getattr(stats, type + "Ease4"))
|
|
||||||
# total for type, eg. gNewTotal
|
|
||||||
h[pre + type.capitalize() + "Total"] = (
|
|
||||||
h[pre + type.capitalize() + "No"] +
|
|
||||||
h[pre + type.capitalize() + "Yes"])
|
|
||||||
# total yes/no, eg. gYesTotal
|
|
||||||
for answer in ("yes", "no"):
|
|
||||||
num = 0
|
|
||||||
for type in cardTypes:
|
|
||||||
num += h[pre + type.capitalize() + answer.capitalize()]
|
|
||||||
h[pre + answer.capitalize() + "Total"] = num
|
|
||||||
# total over all, eg. gTotal
|
|
||||||
num = 0
|
|
||||||
for type in cardTypes:
|
|
||||||
num += h[pre + type.capitalize() + "Total"]
|
|
||||||
h[pre + "Total"] = num
|
|
||||||
# percentages
|
|
||||||
##############
|
|
||||||
for type in cardTypes:
|
|
||||||
# total yes/no % by type, eg. gNewYes%
|
|
||||||
for answer in ("yes", "no"):
|
|
||||||
setPercentage(h, pre + type.capitalize() + answer.capitalize(),
|
|
||||||
pre + type.capitalize())
|
|
||||||
for answer in ("yes", "no"):
|
|
||||||
# total yes/no, eg. gYesTotal%
|
|
||||||
setPercentage(h, pre + answer.capitalize() + "Total", pre)
|
|
||||||
h[pre + 'AverageTime'] = stats.averageTime
|
|
||||||
h[pre + 'ReviewTime'] = stats.reviewTime
|
|
||||||
return h
|
|
||||||
|
|
||||||
def setPercentage(h, a, b):
|
|
||||||
try:
|
|
||||||
h[a + "%"] = (h[a] / float(h[b + "Total"])) * 100
|
|
||||||
except ZeroDivisionError:
|
|
||||||
h[a + "%"] = 0
|
|
||||||
|
|
||||||
def getStats(s, gs, ds):
|
|
||||||
"Return a handy dictionary exposing a number of internal stats."
|
|
||||||
h = {}
|
|
||||||
h.update(summarizeStats(gs, "g"))
|
|
||||||
h.update(summarizeStats(ds, "d"))
|
|
||||||
return h
|
|
||||||
|
|
||||||
# Card stats
|
# Card stats
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -295,7 +62,7 @@ class CardStats(object):
|
||||||
s = anki.utils.fmtTimeSpan(time.time() - tm)
|
s = anki.utils.fmtTimeSpan(time.time() - tm)
|
||||||
return _("%s ago") % s
|
return _("%s ago") % s
|
||||||
|
|
||||||
# Deck stats (specific to the 'sched' scheduler)
|
# Deck stats
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
class DeckStats(object):
|
class DeckStats(object):
|
||||||
|
@ -355,7 +122,7 @@ class DeckStats(object):
|
||||||
'partOf' : nYes,
|
'partOf' : nYes,
|
||||||
'totalSum' : nAll } + "<br><br>")
|
'totalSum' : nAll } + "<br><br>")
|
||||||
# average pending time
|
# average pending time
|
||||||
existing = d.cardCount - d.newCountToday
|
existing = d.cardCount - d.newCount
|
||||||
def tr(a, b):
|
def tr(a, b):
|
||||||
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
|
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
|
||||||
def repsPerDay(reps,days):
|
def repsPerDay(reps,days):
|
||||||
|
|
54
anki/sync.py
54
anki/sync.py
|
@ -11,9 +11,7 @@ from anki.errors import *
|
||||||
from anki.models import Model, FieldModel, CardModel
|
from anki.models import Model, FieldModel, CardModel
|
||||||
from anki.facts import Fact, Field
|
from anki.facts import Fact, Field
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.stats import Stats, globalStats
|
|
||||||
from anki.history import CardHistoryEntry
|
from anki.history import CardHistoryEntry
|
||||||
from anki.stats import globalStats
|
|
||||||
from anki.utils import ids2str, hexifyID, checksum
|
from anki.utils import ids2str, hexifyID, checksum
|
||||||
from anki.media import mediaFiles
|
from anki.media import mediaFiles
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
|
@ -114,7 +112,6 @@ class SyncTools(object):
|
||||||
|
|
||||||
def genPayload(self, summaries):
|
def genPayload(self, summaries):
|
||||||
(lsum, rsum) = summaries
|
(lsum, rsum) = summaries
|
||||||
self.preSyncRefresh()
|
|
||||||
payload = {}
|
payload = {}
|
||||||
# first, handle models, facts and cards
|
# first, handle models, facts and cards
|
||||||
for key in KEYS:
|
for key in KEYS:
|
||||||
|
@ -125,7 +122,6 @@ class SyncTools(object):
|
||||||
self.deleteObjsFromKey(diff[3], key)
|
self.deleteObjsFromKey(diff[3], key)
|
||||||
# handle the remainder
|
# handle the remainder
|
||||||
if self.localTime > self.remoteTime:
|
if self.localTime > self.remoteTime:
|
||||||
payload['stats'] = self.bundleStats()
|
|
||||||
payload['history'] = self.bundleHistory()
|
payload['history'] = self.bundleHistory()
|
||||||
payload['sources'] = self.bundleSources()
|
payload['sources'] = self.bundleSources()
|
||||||
# finally, set new lastSync and bundle the deck info
|
# finally, set new lastSync and bundle the deck info
|
||||||
|
@ -134,7 +130,6 @@ class SyncTools(object):
|
||||||
|
|
||||||
def applyPayload(self, payload):
|
def applyPayload(self, payload):
|
||||||
reply = {}
|
reply = {}
|
||||||
self.preSyncRefresh()
|
|
||||||
# model, facts and cards
|
# model, facts and cards
|
||||||
for key in KEYS:
|
for key in KEYS:
|
||||||
k = 'added-' + key
|
k = 'added-' + key
|
||||||
|
@ -146,14 +141,12 @@ class SyncTools(object):
|
||||||
self.deleteObjsFromKey(payload['deleted-' + key], key)
|
self.deleteObjsFromKey(payload['deleted-' + key], key)
|
||||||
# send back deck-related stuff if it wasn't sent to us
|
# send back deck-related stuff if it wasn't sent to us
|
||||||
if not 'deck' in payload:
|
if not 'deck' in payload:
|
||||||
reply['stats'] = self.bundleStats()
|
|
||||||
reply['history'] = self.bundleHistory()
|
reply['history'] = self.bundleHistory()
|
||||||
reply['sources'] = self.bundleSources()
|
reply['sources'] = self.bundleSources()
|
||||||
# finally, set new lastSync and bundle the deck info
|
# finally, set new lastSync and bundle the deck info
|
||||||
reply['deck'] = self.bundleDeck()
|
reply['deck'] = self.bundleDeck()
|
||||||
else:
|
else:
|
||||||
self.updateDeck(payload['deck'])
|
self.updateDeck(payload['deck'])
|
||||||
self.updateStats(payload['stats'])
|
|
||||||
self.updateHistory(payload['history'])
|
self.updateHistory(payload['history'])
|
||||||
if 'sources' in payload:
|
if 'sources' in payload:
|
||||||
self.updateSources(payload['sources'])
|
self.updateSources(payload['sources'])
|
||||||
|
@ -172,7 +165,6 @@ class SyncTools(object):
|
||||||
# deck
|
# deck
|
||||||
if 'deck' in reply:
|
if 'deck' in reply:
|
||||||
self.updateDeck(reply['deck'])
|
self.updateDeck(reply['deck'])
|
||||||
self.updateStats(reply['stats'])
|
|
||||||
self.updateHistory(reply['history'])
|
self.updateHistory(reply['history'])
|
||||||
if 'sources' in reply:
|
if 'sources' in reply:
|
||||||
self.updateSources(reply['sources'])
|
self.updateSources(reply['sources'])
|
||||||
|
@ -194,10 +186,6 @@ class SyncTools(object):
|
||||||
self.deck.s.refresh(self.deck)
|
self.deck.s.refresh(self.deck)
|
||||||
self.deck.currentModel
|
self.deck.currentModel
|
||||||
|
|
||||||
def preSyncRefresh(self):
|
|
||||||
# ensure global stats are available (queue may not be built)
|
|
||||||
self.deck._globalStats = globalStats(self.deck)
|
|
||||||
|
|
||||||
# Summaries
|
# Summaries
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -553,7 +541,7 @@ values
|
||||||
def deleteCards(self, ids):
|
def deleteCards(self, ids):
|
||||||
self.deck.deleteCards(ids)
|
self.deck.deleteCards(ids)
|
||||||
|
|
||||||
# Deck/stats/history
|
# Deck/history
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def bundleDeck(self):
|
def bundleDeck(self):
|
||||||
|
@ -595,42 +583,6 @@ insert or replace into deckVars
|
||||||
del deck['meta']
|
del deck['meta']
|
||||||
self.applyDict(self.deck, deck)
|
self.applyDict(self.deck, deck)
|
||||||
|
|
||||||
def bundleStats(self):
|
|
||||||
def bundleStat(stat):
|
|
||||||
s = self.dictFromObj(stat)
|
|
||||||
s['day'] = s['day'].toordinal()
|
|
||||||
del s['id']
|
|
||||||
return s
|
|
||||||
lastDay = date.fromtimestamp(max(0, self.deck.lastSync - 60*60*24))
|
|
||||||
ids = self.deck.s.column0(
|
|
||||||
"select id from stats where type = 1 and day >= :day", day=lastDay)
|
|
||||||
stat = Stats()
|
|
||||||
def statFromId(id):
|
|
||||||
stat.fromDB(self.deck.s, id)
|
|
||||||
return stat
|
|
||||||
stats = {
|
|
||||||
'global': bundleStat(self.deck._globalStats),
|
|
||||||
'daily': [bundleStat(statFromId(id)) for id in ids],
|
|
||||||
}
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def updateStats(self, stats):
|
|
||||||
stats['global']['day'] = date.fromordinal(stats['global']['day'])
|
|
||||||
self.applyDict(self.deck._globalStats, stats['global'])
|
|
||||||
self.deck._globalStats.toDB(self.deck.s)
|
|
||||||
for record in stats['daily']:
|
|
||||||
record['day'] = date.fromordinal(record['day'])
|
|
||||||
stat = Stats()
|
|
||||||
id = self.deck.s.scalar("select id from stats where "
|
|
||||||
"type = :type and day = :day",
|
|
||||||
type=1, day=record['day'])
|
|
||||||
if id:
|
|
||||||
stat.fromDB(self.deck.s, id)
|
|
||||||
else:
|
|
||||||
stat.create(self.deck.s, 1, record['day'])
|
|
||||||
self.applyDict(stat, record)
|
|
||||||
stat.toDB(self.deck.s)
|
|
||||||
|
|
||||||
def bundleHistory(self):
|
def bundleHistory(self):
|
||||||
return self.realLists(self.deck.s.all("""
|
return self.realLists(self.deck.s.all("""
|
||||||
select cardId, time, lastInterval, nextInterval, ease, delay,
|
select cardId, time, lastInterval, nextInterval, ease, delay,
|
||||||
|
@ -886,10 +838,6 @@ and cards.id in %s""" % ids2str([c[0] for c in cards])))
|
||||||
ls=self.deck.lastSync) > 1000:
|
ls=self.deck.lastSync) > 1000:
|
||||||
return True
|
return True
|
||||||
lastDay = date.fromtimestamp(max(0, self.deck.lastSync - 60*60*24))
|
lastDay = date.fromtimestamp(max(0, self.deck.lastSync - 60*60*24))
|
||||||
if self.deck.s.scalar(
|
|
||||||
"select count() from stats where day >= :day",
|
|
||||||
day=lastDay) > 100:
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def prepareFullSync(self):
|
def prepareFullSync(self):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
DECK_VERSION = 72
|
DECK_VERSION = 73
|
||||||
|
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
from anki.media import rebuildMediaDir
|
from anki.media import rebuildMediaDir
|
||||||
|
@ -45,9 +45,6 @@ create index if not exists ix_cards_priority on cards
|
||||||
# card spacing
|
# card spacing
|
||||||
deck.s.statement("""
|
deck.s.statement("""
|
||||||
create index if not exists ix_cards_factId on cards (factId)""")
|
create index if not exists ix_cards_factId on cards (factId)""")
|
||||||
# stats
|
|
||||||
deck.s.statement("""
|
|
||||||
create index if not exists ix_stats_typeDay on stats (type, day)""")
|
|
||||||
# fields
|
# fields
|
||||||
deck.s.statement("""
|
deck.s.statement("""
|
||||||
create index if not exists ix_fields_factId on fields (factId)""")
|
create index if not exists ix_fields_factId on fields (factId)""")
|
||||||
|
@ -230,10 +227,18 @@ this message. (ERR-0101)""") % {
|
||||||
deck.version = 71
|
deck.version = 71
|
||||||
deck.s.commit()
|
deck.s.commit()
|
||||||
if deck.version < 72:
|
if deck.version < 72:
|
||||||
# remove the expensive value cache
|
# this was only used for calculating average factor
|
||||||
deck.s.statement("drop index if exists ix_cards_factor")
|
deck.s.statement("drop index if exists ix_cards_factor")
|
||||||
deck.version = 72
|
deck.version = 72
|
||||||
deck.s.commit()
|
deck.s.commit()
|
||||||
|
if deck.version < 73:
|
||||||
|
# remove stats, as it's all in the revlog now
|
||||||
|
deck.s.statement("drop index if exists ix_stats_typeDay")
|
||||||
|
deck.s.statement("drop table if exists stats")
|
||||||
|
deck.version = 73
|
||||||
|
deck.s.commit()
|
||||||
|
|
||||||
|
|
||||||
# executing a pragma here is very slow on large decks, so we store
|
# executing a pragma here is very slow on large decks, so we store
|
||||||
# our own record
|
# our own record
|
||||||
if not deck.getInt("pageSize") == 4096:
|
if not deck.getInt("pageSize") == 4096:
|
||||||
|
|
|
@ -9,7 +9,6 @@ from anki.db import *
|
||||||
from anki.stdmodels import BasicModel
|
from anki.stdmodels import BasicModel
|
||||||
from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy
|
from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy
|
||||||
from anki.sync import copyLocalMedia
|
from anki.sync import copyLocalMedia
|
||||||
from anki.stats import dailyStats, globalStats
|
|
||||||
from anki.facts import Fact
|
from anki.facts import Fact
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.models import FieldModel
|
from anki.models import FieldModel
|
||||||
|
@ -104,8 +103,6 @@ def test_localsync_deck():
|
||||||
c = deck1.getCard()
|
c = deck1.getCard()
|
||||||
deck1.answerCard(c, 4)
|
deck1.answerCard(c, 4)
|
||||||
client.sync()
|
client.sync()
|
||||||
assert dailyStats(deck2).reps == 1
|
|
||||||
assert globalStats(deck2).reps == 1
|
|
||||||
assert deck2.s.scalar("select count(*) from reviewHistory") == 1
|
assert deck2.s.scalar("select count(*) from reviewHistory") == 1
|
||||||
# make sure meta data is synced
|
# make sure meta data is synced
|
||||||
deck1.setVar("foo", 1)
|
deck1.setVar("foo", 1)
|
||||||
|
|
Loading…
Reference in a new issue