diff --git a/anki/deck.py b/anki/deck.py index 0c7089b12..a3f0899a0 100644 --- a/anki/deck.py +++ b/anki/deck.py @@ -51,17 +51,42 @@ SEARCH_FIELD_EXISTS = 7 SEARCH_QA = 8 SEARCH_PHRASE_WB = 9 -deckVarsTable = Table( - 'deckVars', metadata, - Column('key', UnicodeText, nullable=False, primary_key=True), - Column('value', UnicodeText)) +# selective study +defaultLim = { + 'newActive': u"", + 'newInactive': u"", + 'revActive': u"", + 'revInactive': u"", +} + +# scheduling and other options +defaultConf = { + 'utcOffset': -2, + 'newCardOrder': 1, + 'newCardSpacing': NEW_CARDS_DISTRIBUTE, + 'newCardsPerDay': 20, + 'revCardOrder': 0, + 'collapseTime': 600, + 'sessionRepLimit': 0, + 'sessionTimeLimit': 600, + 'suspendLeeches': True, + 'leechFails': 16, + 'currentModelId': None, + 'mediaURL': "", + 'latexPre': """\ +\\documentclass[12pt]{article} +\\special{papersize=3in,5in} +\\usepackage[utf8]{inputenc} +\\usepackage{amssymb,amsmath} +\\pagestyle{empty} +\\setlength{\\parindent}{0in} +\\begin{document} +""", + 'latexPost': "\\end{document}", +} # syncName: md5sum of current deck location, to detect if deck was moved or # renamed. mobile clients can treat this as a simple boolean - -# utcOffset: store independent of tz? - -# parts of the code assume we only have one deck deckTable = Table( 'deck', metadata, Column('id', Integer, nullable=False, primary_key=True), @@ -69,27 +94,14 @@ deckTable = Table( Column('modified', Float, nullable=False, default=time.time), Column('schemaMod', Float, nullable=False, default=0), Column('version', Integer, nullable=False, default=DECK_VERSION), - Column('currentModelId', Integer, ForeignKey("models.id")), Column('syncName', UnicodeText, nullable=False, default=u""), - Column('lastSync', Float, nullable=False, default=0), - # scheduling + Column('lastSync', Integer, nullable=False, default=0), Column('utcOffset', Integer, nullable=False, default=-2), - Column('newCardOrder', Integer, nullable=False, default=1), - Column('newCardSpacing', Integer, nullable=False, default=NEW_CARDS_DISTRIBUTE), - Column('newCardsPerDay', Integer, nullable=False, default=20), - Column('revCardOrder', Integer, nullable=False, default=0), - Column('collapseTime', Integer, nullable=False, default=600), - # timeboxing - Column('sessionRepLimit', Integer, nullable=False, default=0), - Column('sessionTimeLimit', Integer, nullable=False, default=600), - # leeches - Column('suspendLeeches', Boolean, nullable=False, default=True), - Column('leechFails', Integer, nullable=False, default=16), - # selective study - Column('newActive', UnicodeText, nullable=False, default=u""), - Column('newInactive', UnicodeText, nullable=False, default=u""), - Column('revActive', UnicodeText, nullable=False, default=u""), - Column('revInactive', UnicodeText, nullable=False, default=u""), + Column('limits', UnicodeText, nullable=False, default=unicode( + simplejson.dumps(defaultLim))), + Column('config', UnicodeText, nullable=False, default=unicode( + simplejson.dumps(defaultConf))), + Column('data', UnicodeText, nullable=False, default=u"{}") ) class Deck(object): @@ -108,19 +120,6 @@ class Deck(object): self.sessionStartReps = 0 self.sessionStartTime = 0 self.lastSessionStart = 0 - # if most recent deck var not defined, make sure defaults are set - if not self.db.scalar("select 1 from deckVars where key = 'latexPost'"): - self.setVarDefault("mediaURL", "") - self.setVarDefault("latexPre", """\ -\\documentclass[12pt]{article} -\\special{papersize=3in,5in} -\\usepackage[utf8]{inputenc} -\\usepackage{amssymb,amsmath} -\\pagestyle{empty} -\\setlength{\\parindent}{0in} -\\begin{document} -""") - self.setVarDefault("latexPost", "\\end{document}") self.sched = Scheduler(self) def modifiedSinceSave(self): @@ -733,7 +732,7 @@ select id, null, null, null, questionAlign, 0, 0 from cardModels""") (hexifyID(row[0]), row[1]) for row in self.db.all(""" select id, lastFontColour from cardModels""")]) self.css = css - self.setVar("cssCache", css, mod=False) + self.data['cssCache'] = css self.addHexCache() return css @@ -745,7 +744,7 @@ select id from models""") cache = {} for id in ids: cache[id] = hexifyID(id) - self.setVar("hexCache", simplejson.dumps(cache), mod=False) + self.data['hexCache'] = cache def copyModel(self, oldModel): "Add a new model to DB based on MODEL." @@ -2163,8 +2162,15 @@ Return new path, relative to media dir.""" if self.lastLoaded == self.modified: return self.lastLoaded = self.modified + self.flushConfig() self.db.commit() + def flushConfig(self): + print "make flushConfig() more intelligent" + deck._config = simplejson.dumps(deck.config) + deck._limits = simplejson.dumps(deck.limits) + deck._data = simplejson.dumps(deck.data) + def close(self): if self.db: self.db.rollback() @@ -2225,6 +2231,7 @@ Return new path, relative to media dir.""" def saveAs(self, newPath): "Returns new deck. Old connection is closed without saving." oldMediaDir = self.mediaDir() + self.flushConfig() self.db.flush() # remove new deck if it exists try: @@ -2648,6 +2655,8 @@ seq > :s and seq <= :e order by seq desc""", s=start, e=end) ########################################################################## def updateDynamicIndices(self): + print "fix dynamicIndices()" + return indices = { 'intervalDesc': '(queue, interval desc, factId, due)', @@ -2690,7 +2699,11 @@ seq > :s and seq <= :e order by seq desc""", s=start, e=end) if analyze: self.db.statement("analyze") -mapper(Deck, deckTable) +mapper(Deck, deckTable, properties={ + '_limits': deckTable.c.limits, + '_config': deckTable.c.config, + '_data': deckTable.c.data, +}) # Shared decks ########################################################################## @@ -2825,10 +2838,14 @@ class DeckStorage(object): path = os.path.abspath(path) create = not os.path.exists(path) deck = DeckStorage._getDeck(path, create, pool) + deck.limits = simplejson.loads(deck._limits) if not rebuild: # minimal startup return deck oldMod = deck.modified + # setup config + deck.config = simplejson.loads(deck._config) + deck.data = simplejson.loads(deck._data) # unsuspend buried/rev early deck.db.statement( "update cards set queue = type where queue between -3 and -2") diff --git a/anki/sched.py b/anki/sched.py index 5c24614ab..91aa086d5 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -6,7 +6,8 @@ import time, datetime, simplejson from heapq import * from anki.db import * from anki.cards import Card -from anki.utils import parseTags +from anki.utils import parseTags, ids2str +from anki.tags import tagIds from anki.lang import _ # the standard Anki scheduler @@ -503,8 +504,8 @@ limit %d""" % (self.newOrder(), self.queueLimit)), lim=self.dayCutoff) "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)) + yes = parseTags(self.deck.limits.get(active)) + no = parseTags(self.deck.limits.get(inactive)) if yes: yids = tagIds(self.db, yes).values() nids = tagIds(self.db, no).values() diff --git a/anki/upgrade.py b/anki/upgrade.py index 6c8fe9a78..c57d36cbb 100644 --- a/anki/upgrade.py +++ b/anki/upgrade.py @@ -4,7 +4,7 @@ DECK_VERSION = 100 -import time +import time, simplejson from anki.db import * from anki.lang import _ from anki.media import rebuildMediaDir @@ -38,7 +38,7 @@ def upgradeSchema(engine, s): metadata.create_all(engine, tables=[cards.cardsTable]) s.execute(""" insert into cards select id, factId, -(select modelId from facts where facts.id = cards.factId), +(select modelId from facts where facts.id = cards2.factId), cardModelId, created, modified, question, answer, ordinal, 0, relativeDelay, type, due, interval, factor, reps, successive, noCount, 0, 0 from cards2""") @@ -73,15 +73,7 @@ originalPath from media2""") s.execute("drop table media2") # deck ########### - import deck - metadata.create_all(engine, tables=[deck.deckTable]) - s.execute(""" -insert into deck select id, created, modified, 0, 99, currentModelId, -ifnull(syncName, ""), lastSync, utcOffset, newCardOrder, -newCardSpacing, newCardsPerDay, revCardOrder, 600, sessionRepLimit, -sessionTimeLimit, 1, 16, '', '', '', '' from decks - """) - s.execute("drop table decks") + migrateDeck(s, engine) # models ########### moveTable(s, "models") @@ -89,13 +81,46 @@ sessionTimeLimit, 1, 16, '', '', '', '' from decks metadata.create_all(engine, tables=[models.modelsTable]) s.execute(""" insert or ignore into models select id, created, modified, name, -'[0.5, 3, 10]', '[1, 7, 4]', -'[0.5, 3, 10]', '[1, 7, 4]', -0, 2.5 from models2""") +:c from models2""", {'c':simplejson.dumps(models.defaultConf)}) s.execute("drop table models2") return ver +def migrateDeck(s, engine): + import deck + metadata.create_all(engine, tables=[deck.deckTable]) + s.execute(""" +insert into deck select id, created, modified, 0, 99, +ifnull(syncName, ""), lastSync, utcOffset, "", "", "" from decks""") + # update selective study + lim = deck.defaultLim.copy() + keys = ("newActive", "newInactive", "revActive", "revInactive") + for k in keys: + lim[k] = s.execute("select value from deckVars where key=:k", + {'k':k}).scalar() + s.execute("delete from deckVars where key=:k", {'k':k}) + # fetch remaining settings from decks table + conf = deck.defaultConf.copy() + data = {} + keys = ("newCardOrder", "newCardSpacing", "newCardsPerDay", + "revCardOrder", "sessionRepLimit", "sessionTimeLimit") + for k in keys: + conf[k] = s.execute("select %s from decks" % k).scalar() + # add any deck vars and save + dkeys = ("hexCache", "cssCache") + for (k, v) in s.execute("select * from deckVars").fetchall(): + if k in dkeys: + data[k] = v + else: + conf[k] = v + s.execute("update deck set limits = :l, config = :c, data = :d", + {'l':simplejson.dumps(lim), + 'c':simplejson.dumps(conf), + 'd':simplejson.dumps(data)}) + # clean up + s.execute("drop table decks") + s.execute("drop table deckVars") + def updateIndices(db): "Add indices to the DB." # due counts, failed card queue diff --git a/tests/test_deck.py b/tests/test_deck.py index 11ff0b55c..3ed288cb5 100644 --- a/tests/test_deck.py +++ b/tests/test_deck.py @@ -1,6 +1,6 @@ # coding: utf-8 -import nose, os, re +import nose, os, re, tempfile, shutil from tests.shared import assertException, getDeck from anki.errors import * @@ -307,3 +307,9 @@ def test_findCards(): c = deck.addFact(f) assert len(deck.findCards('tag:forward')) == 5 assert len(deck.findCards('tag:reverse')) == 1 + +def test_upgrade(): + src = os.path.expanduser("~/Scratch/upgrade.anki") + (fd, dst) = tempfile.mkstemp(suffix=".anki") + shutil.copy(src, dst) + deck = Deck(dst)