diff --git a/anki/consts.py b/anki/consts.py index c3ada3dac..29f2f1e7c 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -44,6 +44,9 @@ SYNC_PORT = int(os.environ.get("SYNC_PORT") or 80) SYNC_URL = "http://%s:%d/sync/" % (SYNC_HOST, SYNC_PORT) SYNC_VER = 0 +# deck schema +SCHEMA_VERSION = 1 + # Labels ########################################################################## diff --git a/anki/importing/mnemosyne10.py b/anki/importing/mnemosyne10.py deleted file mode 100644 index b039b30b3..000000000 --- a/anki/importing/mnemosyne10.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: Damien Elmes -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -import sys, pickle, time, re -from anki.importing import Importer, ForeignCard -from anki.errors import * - -class Mnemosyne10Importer(Importer): - - multipleCardsAllowed = False - - def foreignCards(self): - # empty objects so we can load the native mnemosyne file - class MnemosyneModule(object): - class StartTime: - pass - class Category: - pass - class Item: - pass - for module in ('mnemosyne', - 'mnemosyne.core', - 'mnemosyne.core.mnemosyne_core'): - sys.modules[module] = MnemosyneModule() - try: - file = open(self.file, "rb") - except (IOError, OSError), e: - raise ImportFormatError(type="systemError", - info=str(e)) - header = file.readline().strip() - # read the structure in - try: - struct = pickle.load(file) - except (EOFError, KeyError): - raise ImportFormatError(type="invalidFile") - startTime = struct[0].time - daysPassed = (time.time() - startTime) / 86400.0 - # gather cards - cards = [] - for item in struct[2]: - card = ForeignCard() - card.fields.append(self.fudgeText(item.q)) - card.fields.append(self.fudgeText(item.a)) - # scheduling data - card.interval = item.next_rep - item.last_rep - secDelta = (item.next_rep - daysPassed) * 86400.0 - card.due = card.nextTime = time.time() + secDelta - card.factor = item.easiness - # for some reason mnemosyne starts cards off on 1 instead of 0 - card.successive = max( - (item.acq_reps_since_lapse + item.ret_reps_since_lapse -1), 0) - card.yesCount = max((item.acq_reps + item.ret_reps) - 1, 0) - card.noCount = item.lapses - card.reps = card.yesCount + card.noCount - if item.cat.name != u"": - card.tags = item.cat.name.replace(" ", "_") - cards.append(card) - return cards - - def fields(self): - return 2 - - def fudgeText(self, text): - text = text.replace("\n", "
") - text = re.sub('', '[sound:\\1]', text) - text = re.sub('<(/?latex)>', '[\\1]', text) - text = re.sub('<(/?\$)>', '[\\1]', text) - text = re.sub('<(/?\$\$)>', '[\\1]', text) - return text diff --git a/anki/migration/upgrader.py b/anki/migration/upgrader.py index 8a2553c3f..60bb657b9 100644 --- a/anki/migration/upgrader.py +++ b/anki/migration/upgrader.py @@ -2,139 +2,112 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/copyleft/agpl.html -import os, time, simplejson, re, datetime +import os, time, simplejson, re, datetime, shutil from anki.lang import _ -from anki.utils import intTime +from anki.utils import intTime, namedtmp from anki.db import DB from anki.deck import _Deck +from anki.consts import * +from anki.storage import _addSchema, _getDeckVars, _addDeckVars, \ + _updateIndices -def Deck(path, queue=True, lock=True, server=False): - "Open a new or existing deck. Path must be unicode." - path = os.path.abspath(path) - create = not os.path.exists(path) - if create: - base = os.path.basename(path) - for c in ("/", ":", "\\"): - assert c not in base - # connect - db = DB(path) - if create: - ver = _createDB(db) - else: - ver = _upgradeSchema(db) - db.execute("pragma temp_store = memory") - db.execute("pragma cache_size = 10000") - # add db to deck and do any remaining upgrades - deck = _Deck(db, server) - if ver < CURRENT_VERSION: - _upgradeDeck(deck, ver) - elif create: - # add in reverse order so basic is default - addClozeModel(deck) - addBasicModel(deck) - deck.save() - if lock: - deck.lock() - if not queue: - return deck - # rebuild queue - deck.reset() - return deck +# +# Upgrading is the first step in migrating to 2.0. The ids are temporary and +# may not be unique across multiple decks. After each of a user's v1.2 decks +# are upgraded, they need to be merged. +# +# Caller should have called check() on path before using this. +# -# 2.0 schema migration -###################################################################### +class Upgrader(object): -def _moveTable(db, table, cards=False): - if cards: - insExtra = " order by created" - else: - insExtra = "" - sql = db.scalar( - "select sql from sqlite_master where name = '%s'" % table) - sql = sql.replace("TABLE "+table, "temporary table %s2" % table) - if cards: - sql = sql.replace("PRIMARY KEY (id),", "") - db.execute(sql) - db.execute("insert into %s2 select * from %s%s" % (table, table, insExtra)) - db.execute("drop table "+table) - _addSchema(db, False) + def __init__(self): + pass -def _upgradeSchema(db): - "Alter tables prior to ORM initialization." - try: - ver = db.scalar("select ver from deck") - except: - ver = db.scalar("select version from decks") - # latest 1.2 is 65 - if ver < 65: - raise AnkiError("oldDeckVersion") - if ver > 99: - # anki 2.0 - if ver > CURRENT_VERSION: - # refuse to load decks created with a future version - raise AnkiError("newDeckVersion") - return ver - runHook("1.x upgrade", db) + def upgrade(self, path): + self.path = path + self._openDB(path) + self._upgradeSchema() + self._openDeck() + self._upgradeDeck() + return self.deck - # these weren't always correctly set - db.execute("pragma page_size = 4096") - db.execute("pragma legacy_file_format = 0") + def _openDB(self, path): + self.tmppath = namedtmp(os.path.basename(path)) + shutil.copy(path, self.tmppath) + self.db = DB(self.tmppath) - # facts - ########### - # tags should have a leading and trailing space if not empty, and not - # use commas - db.execute(""" + def _openDeck(self): + self.deck = _Deck(self.db) + + # Schema upgrade + ###################################################################### + + def _upgradeSchema(self): + "Alter tables prior to ORM initialization." + db = self.db + # speed up the upgrade + db.execute("pragma temp_store = memory") + db.execute("pragma cache_size = 10000") + # these weren't always correctly set + db.execute("pragma page_size = 4096") + db.execute("pragma legacy_file_format = 0") + + # facts + ########### + # tags should have a leading and trailing space if not empty, and not + # use commas + db.execute(""" update facts set tags = (case when trim(tags) == "" then "" else " " || replace(replace(trim(tags), ",", " "), " ", " ") || " " end) """) - # pull facts into memory, so we can merge them with fields efficiently - facts = db.all(""" -select id, id, modelId, 1, cast(created*1000 as int), cast(modified as int), 0, tags -from facts order by created""") - # build field hash - fields = {} - for (fid, ord, val) in db.execute( - "select factId, ordinal, value from fields order by factId, ordinal"): - if fid not in fields: - fields[fid] = [] - fields[fid].append((ord, val)) - # build insert data and transform ids, and minimize qt's - # bold/italics/underline cruft. - map = {} - data = [] - factidmap = {} - times = {} - from anki.utils import minimizeHTML - for c, row in enumerate(facts): - oldid = row[0] - row = list(row) - # get rid of old created column and update id - while row[4] in times: - row[4] += 1 - times[row[4]] = True - factidmap[row[0]] = row[4] - row[0] = row[4] - del row[4] - map[oldid] = row[0] - row.append(minimizeHTML("\x1f".join([x[1] for x in sorted(fields[oldid])]))) - data.append(row) - # and put the facts into the new table - db.execute("drop table facts") - _addSchema(db, False) - db.executemany("insert into facts values (?,?,?,?,?,?,?,?,'',0,'')", data) - db.execute("drop table fields") + # pull facts into memory, so we can merge them with fields efficiently + facts = db.all(""" +select id, id, modelId, 1, cast(created*1000 as int), cast(modified as int), +0, tags from facts order by created""") + # build field hash + fields = {} + for (fid, ord, val) in db.execute( + "select factId, ordinal, value from fields order by factId, ordinal"): + if fid not in fields: + fields[fid] = [] + fields[fid].append((ord, val)) + # build insert data and transform ids, and minimize qt's + # bold/italics/underline cruft. + map = {} + data = [] + factidmap = {} + times = {} + from anki.utils import minimizeHTML + for c, row in enumerate(facts): + oldid = row[0] + row = list(row) + # get rid of old created column and update id + while row[4] in times: + row[4] += 1 + times[row[4]] = True + factidmap[row[0]] = row[4] + row[0] = row[4] + del row[4] + map[oldid] = row[0] + row.append(minimizeHTML("\x1f".join([x[1] for x in sorted(fields[oldid])]))) + data.append(row) + # and put the facts into the new table + db.execute("drop table facts") + _addSchema(db, False) + db.executemany("insert into facts values (?,?,?,?,?,?,?,?,'',0,'')", data) + db.execute("drop table fields") - # cards - ########### - # we need to pull this into memory, to rewrite the creation time if - # it's not unique and update the fact id - times = {} - rows = [] - cardidmap = {} - for row in db.execute(""" + # cards + ########### + # we need to pull this into memory, to rewrite the creation time if + # it's not unique and update the fact id + times = {} + rows = [] + cardidmap = {} + for row in db.execute(""" select id, cast(created*1000 as int), factId, ordinal, cast(modified as int), 0, (case relativeDelay @@ -149,286 +122,283 @@ else type end), cast(due as int), cast(interval as int), cast(factor*1000 as int), reps, noCount from cards order by created"""): - # find an unused time - row = list(row) - while row[1] in times: - row[1] += 1 - times[row[1]] = True - # rewrite fact id - row[2] = factidmap[row[2]] - # note id change and save all but old id - cardidmap[row[0]] = row[1] - rows.append(row[1:]) - # drop old table and rewrite - db.execute("drop table cards") - _addSchema(db, False) - db.executemany(""" + # find an unused time + row = list(row) + while row[1] in times: + row[1] += 1 + times[row[1]] = True + # rewrite fact id + row[2] = factidmap[row[2]] + # note id change and save all but old id + cardidmap[row[0]] = row[1] + rows.append(row[1:]) + # drop old table and rewrite + db.execute("drop table cards") + _addSchema(db, False) + db.executemany(""" insert into cards values (?,?,1,?,?,?,?,?,?,?,?,?,?,0,0,0,"")""", - rows) + rows) - # reviewHistory -> revlog - ########### - # fetch the data so we can rewrite ids quickly - r = [] - for row in db.execute(""" + # reviewHistory -> revlog + ########### + # fetch the data so we can rewrite ids quickly + r = [] + for row in db.execute(""" select cast(time*1000 as int), cardId, 0, ease, cast(nextInterval as int), cast(lastInterval as int), cast(nextFactor*1000 as int), cast(min(thinkingTime, 60)*1000 as int), yesCount from reviewHistory"""): - row = list(row) - # new card ids - try: - row[1] = cardidmap[row[1]] - except: - # id doesn't exist - continue - # no ease 0 anymore - row[2] = row[2] or 1 - # determine type, overwriting yesCount - newInt = row[3] - oldInt = row[4] - yesCnt = row[7] - # yesCnt included the current answer - if row[2] > 1: - yesCnt -= 1 - if oldInt < 1: - # new or failed - if yesCnt: - # type=relrn - row[7] = 2 + row = list(row) + # new card ids + try: + row[1] = cardidmap[row[1]] + except: + # id doesn't exist + continue + # no ease 0 anymore + row[2] = row[2] or 1 + # determine type, overwriting yesCount + newInt = row[3] + oldInt = row[4] + yesCnt = row[7] + # yesCnt included the current answer + if row[2] > 1: + yesCnt -= 1 + if oldInt < 1: + # new or failed + if yesCnt: + # type=relrn + row[7] = 2 + else: + # type=lrn + row[7] = 0 else: - # type=lrn - row[7] = 0 - else: - # type=rev - row[7] = 1 - r.append(row) - db.executemany( - "insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)", r) - db.execute("drop table reviewHistory") + # type=rev + row[7] = 1 + r.append(row) + db.executemany( + "insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)", r) + db.execute("drop table reviewHistory") - # deck - ########### - _migrateDeckTbl(db) + # deck + ########### + self._migrateDeckTbl() - # tags - ########### - tags = {} - for t in db.list("select tag from tags"): - tags[t] = intTime() - db.execute("update deck set tags = ?", simplejson.dumps(tags)) - db.execute("drop table tags") - db.execute("drop table cardTags") + # tags + ########### + tags = {} + for t in db.list("select tag from tags"): + tags[t] = intTime() + db.execute("update deck set tags = ?", simplejson.dumps(tags)) + db.execute("drop table tags") + db.execute("drop table cardTags") - # the rest - ########### - db.execute("drop table media") - db.execute("drop table sources") - _migrateModels(db) - _updateIndices(db) - return ver + # the rest + ########### + db.execute("drop table media") + db.execute("drop table sources") + self._migrateModels() + _updateIndices(db) -def _migrateDeckTbl(db): - import anki.deck - db.execute("delete from deck") - db.execute(""" + def _migrateDeckTbl(self): + import anki.deck + db = self.db + db.execute("delete from deck") + db.execute(""" insert or replace into deck select id, cast(created as int), :t, :t, 99, 0, 0, cast(lastSync as int), "", "", "", "", "" from decks""", t=intTime()) - # prepare a group to store the old deck options - g, gc, conf = _getDeckVars(db) - # delete old selective study settings, which we can't auto-upgrade easily - keys = ("newActive", "newInactive", "revActive", "revInactive") - for k in keys: - db.execute("delete from deckVars where key=:k", k=k) - # copy other settings, ignoring deck order as there's a new default - g['newSpread'] = db.scalar("select newCardSpacing from decks") - g['newPerDay'] = db.scalar("select newCardsPerDay from decks") - g['repLim'] = db.scalar("select sessionRepLimit from decks") - g['timeLim'] = db.scalar("select sessionTimeLimit from decks") - # this needs to be placed in the model later on - conf['oldNewOrder'] = db.scalar("select newCardOrder from decks") - # no reverse option anymore - conf['oldNewOrder'] = min(1, conf['oldNewOrder']) - # add any deck vars and save - dkeys = ("hexCache", "cssCache") - for (k, v) in db.execute("select * from deckVars").fetchall(): - if k in dkeys: - pass - else: - conf[k] = v - _addDeckVars(db, g, gc, conf) - # clean up - db.execute("drop table decks") - db.execute("drop table deckVars") + # prepare a group to store the old deck options + g, gc, conf = _getDeckVars(db) + # delete old selective study settings, which we can't auto-upgrade easily + keys = ("newActive", "newInactive", "revActive", "revInactive") + for k in keys: + db.execute("delete from deckVars where key=:k", k=k) + # copy other settings, ignoring deck order as there's a new default + g['newSpread'] = db.scalar("select newCardSpacing from decks") + g['newPerDay'] = db.scalar("select newCardsPerDay from decks") + g['repLim'] = db.scalar("select sessionRepLimit from decks") + g['timeLim'] = db.scalar("select sessionTimeLimit from decks") + # this needs to be placed in the model later on + conf['oldNewOrder'] = db.scalar("select newCardOrder from decks") + # no reverse option anymore + conf['oldNewOrder'] = min(1, conf['oldNewOrder']) + # add any deck vars and save + dkeys = ("hexCache", "cssCache") + for (k, v) in db.execute("select * from deckVars").fetchall(): + if k in dkeys: + pass + else: + conf[k] = v + _addDeckVars(db, g, gc, conf) + # clean up + db.execute("drop table decks") + db.execute("drop table deckVars") -def _migrateModels(db): - import anki.models - times = {} - mods = {} - for row in db.all( - "select id, name from models"): - while 1: - t = intTime(1000) - if t not in times: - times[t] = True - break - m = anki.models.defaultModel.copy() - m['id'] = t - m['name'] = row[1] - m['mod'] = intTime() - m['tags'] = [] - m['flds'] = _fieldsForModel(db, row[0]) - m['tmpls'] = _templatesForModel(db, row[0], m['flds']) - mods[m['id']] = m - db.execute("update facts set mid = ? where mid = ?", t, row[0]) - # save and clean up - db.execute("update deck set models = ?", simplejson.dumps(mods)) - db.execute("drop table fieldModels") - db.execute("drop table cardModels") - db.execute("drop table models") + def _migrateModels(self): + import anki.models + db = self.db + times = {} + mods = {} + for row in db.all( + "select id, name from models"): + while 1: + t = intTime(1000) + if t not in times: + times[t] = True + break + m = anki.models.defaultModel.copy() + m['id'] = t + m['name'] = row[1] + m['mod'] = intTime() + m['tags'] = [] + m['flds'] = self._fieldsForModel(row[0]) + m['tmpls'] = self._templatesForModel(row[0], m['flds']) + mods[m['id']] = m + db.execute("update facts set mid = ? where mid = ?", t, row[0]) + # save and clean up + db.execute("update deck set models = ?", simplejson.dumps(mods)) + db.execute("drop table fieldModels") + db.execute("drop table cardModels") + db.execute("drop table models") -def _fieldsForModel(db, mid): - import anki.models - dconf = anki.models.defaultField - flds = [] - for c, row in enumerate(db.all(""" + def _fieldsForModel(self, mid): + import anki.models + db = self.db + dconf = anki.models.defaultField + flds = [] + for c, row in enumerate(db.all(""" select name, features, required, "unique", quizFontFamily, quizFontSize, quizFontColour, editFontSize from fieldModels where modelId = ? order by ordinal""", mid)): - conf = dconf.copy() - (conf['name'], - conf['rtl'], - conf['req'], - conf['uniq'], - conf['font'], - conf['qsize'], - conf['qcol'], - conf['esize']) = row - conf['ord'] = c - # ensure data is good - conf['rtl'] = not not conf['rtl'] - conf['pre'] = True - conf['font'] = conf['font'] or "Arial" - conf['qcol'] = conf['qcol'] or "#000" - conf['qsize'] = conf['qsize'] or 20 - conf['esize'] = conf['esize'] or 20 - flds.append(conf) - return flds + conf = dconf.copy() + (conf['name'], + conf['rtl'], + conf['req'], + conf['uniq'], + conf['font'], + conf['qsize'], + conf['qcol'], + conf['esize']) = row + conf['ord'] = c + # ensure data is good + conf['rtl'] = not not conf['rtl'] + conf['pre'] = True + conf['font'] = conf['font'] or "Arial" + conf['qcol'] = conf['qcol'] or "#000" + conf['qsize'] = conf['qsize'] or 20 + conf['esize'] = conf['esize'] or 20 + flds.append(conf) + return flds -def _templatesForModel(db, mid, flds): - import anki.models - dconf = anki.models.defaultTemplate - tmpls = [] - for c, row in enumerate(db.all(""" + def _templatesForModel(self, mid, flds): + import anki.models + db = self.db + dconf = anki.models.defaultTemplate + tmpls = [] + for c, row in enumerate(db.all(""" select name, active, qformat, aformat, questionInAnswer, questionAlign, lastFontColour, allowEmptyAnswer, typeAnswer from cardModels where modelId = ? order by ordinal""", mid)): - conf = dconf.copy() - (conf['name'], - conf['actv'], - conf['qfmt'], - conf['afmt'], - conf['hideQ'], - conf['align'], - conf['bg'], - conf['emptyAns'], - conf['typeAns']) = row - conf['ord'] = c - # convert the field name to an ordinal - ordN = None - for (ord, fm) in enumerate(flds): - if fm['name'] == conf['typeAns']: - ordN = ord - break - if ordN is not None: - conf['typeAns'] = ordN - else: - conf['typeAns'] = None - for type in ("qfmt", "afmt"): - # ensure the new style field format - conf[type] = re.sub("%\((.+?)\)s", "{{\\1}}", conf[type]) - # some special names have changed - conf[type] = re.sub( - "(?i){{tags}}", "{{Tags}}", conf[type]) - conf[type] = re.sub( - "(?i){{cardModel}}", "{{Template}}", conf[type]) - conf[type] = re.sub( - "(?i){{modelTags}}", "{{Model}}", conf[type]) - tmpls.append(conf) - return tmpls + conf = dconf.copy() + (conf['name'], + conf['actv'], + conf['qfmt'], + conf['afmt'], + conf['hideQ'], + conf['align'], + conf['bg'], + conf['emptyAns'], + conf['typeAns']) = row + conf['ord'] = c + # convert the field name to an ordinal + ordN = None + for (ord, fm) in enumerate(flds): + if fm['name'] == conf['typeAns']: + ordN = ord + break + if ordN is not None: + conf['typeAns'] = ordN + else: + conf['typeAns'] = None + for type in ("qfmt", "afmt"): + # ensure the new style field format + conf[type] = re.sub("%\((.+?)\)s", "{{\\1}}", conf[type]) + # some special names have changed + conf[type] = re.sub( + "(?i){{tags}}", "{{Tags}}", conf[type]) + conf[type] = re.sub( + "(?i){{cardModel}}", "{{Template}}", conf[type]) + conf[type] = re.sub( + "(?i){{modelTags}}", "{{Model}}", conf[type]) + tmpls.append(conf) + return tmpls -def _postSchemaUpgrade(deck): - "Handle the rest of the upgrade to 2.0." - import anki.deck - # make sure we have a current model id - deck.models.setCurrent(deck.models.models.values()[0]) - # regenerate css, and set new card order - for m in deck.models.all(): - m['newOrder'] = deck.conf['oldNewOrder'] - deck.models.save(m) - del deck.conf['oldNewOrder'] - # fix creation time - deck.sched._updateCutoff() - d = datetime.datetime.today() - d -= datetime.timedelta(hours=4) - d = datetime.datetime(d.year, d.month, d.day) - d += datetime.timedelta(hours=4) - d -= datetime.timedelta(days=1+int((time.time()-deck.crt)/86400)) - deck.crt = int(time.mktime(d.timetuple())) - deck.sched._updateCutoff() - # update uniq cache - deck.updateFieldCache(deck.db.list("select id from facts")) - # remove old views - for v in ("failedCards", "revCardsOld", "revCardsNew", - "revCardsDue", "revCardsRandom", "acqCardsRandom", - "acqCardsOld", "acqCardsNew"): - deck.db.execute("drop view if exists %s" % v) - # remove stats, as it's all in the revlog now - deck.db.execute("drop table if exists stats") - # suspended cards don't use ranges anymore - deck.db.execute("update cards set queue=-1 where queue between -3 and -1") - deck.db.execute("update cards set queue=-2 where queue between 3 and 5") - deck.db.execute("update cards set queue=-3 where queue between 6 and 8") - # remove old deleted tables - for t in ("cards", "facts", "models", "media"): - deck.db.execute("drop table if exists %sDeleted" % t) - # rewrite due times for new cards - deck.db.execute(""" + # Upgrading deck + ###################################################################### + + def _upgradeDeck(self): + "Handle the rest of the upgrade to 2.0." + import anki.deck + deck = self.deck + # make sure we have a current model id + deck.models.setCurrent(deck.models.models.values()[0]) + # regenerate css, and set new card order + for m in deck.models.all(): + m['newOrder'] = deck.conf['oldNewOrder'] + deck.models.save(m) + del deck.conf['oldNewOrder'] + # fix creation time + deck.sched._updateCutoff() + d = datetime.datetime.today() + d -= datetime.timedelta(hours=4) + d = datetime.datetime(d.year, d.month, d.day) + d += datetime.timedelta(hours=4) + d -= datetime.timedelta(days=1+int((time.time()-deck.crt)/86400)) + deck.crt = int(time.mktime(d.timetuple())) + deck.sched._updateCutoff() + # update uniq cache + deck.updateFieldCache(deck.db.list("select id from facts")) + # remove old views + for v in ("failedCards", "revCardsOld", "revCardsNew", + "revCardsDue", "revCardsRandom", "acqCardsRandom", + "acqCardsOld", "acqCardsNew"): + deck.db.execute("drop view if exists %s" % v) + # remove stats, as it's all in the revlog now + deck.db.execute("drop table if exists stats") + # suspended cards don't use ranges anymore + deck.db.execute("update cards set queue=-1 where queue between -3 and -1") + deck.db.execute("update cards set queue=-2 where queue between 3 and 5") + deck.db.execute("update cards set queue=-3 where queue between 6 and 8") + # remove old deleted tables + for t in ("cards", "facts", "models", "media"): + deck.db.execute("drop table if exists %sDeleted" % t) + # rewrite due times for new cards + deck.db.execute(""" update cards set due = fid where type=0""") - # and failed cards - left = len(deck.groups.conf(1)['new']['delays']) - deck.db.execute("update cards set edue = ?, left=? where type = 1", - deck.sched.today+1, left) - # and due cards - deck.db.execute(""" + # and failed cards + left = len(deck.groups.conf(1)['new']['delays']) + deck.db.execute("update cards set edue = ?, left=? where type = 1", + deck.sched.today+1, left) + # and due cards + deck.db.execute(""" update cards set due = cast( (case when due < :stamp then 0 else 1 end) + ((due-:stamp)/86400) as int)+:today where type = 2 """, stamp=deck.sched.dayCutoff, today=deck.sched.today) - # possibly re-randomize - if deck.models.randomNew(): - deck.sched.randomizeCards() - # update insertion id - deck.conf['nextPos'] = deck.db.scalar("select max(id) from facts")+1 - deck.save() + # possibly re-randomize + if deck.models.randomNew(): + deck.sched.randomizeCards() + # update insertion id + deck.conf['nextPos'] = deck.db.scalar("select max(id) from facts")+1 + deck.save() - # optimize and finish - deck.db.commit() - deck.db.execute("vacuum") - deck.db.execute("analyze") - deck.db.execute("update deck set ver = ?", CURRENT_VERSION) - deck.save() - -# Post-init upgrade -###################################################################### - -def _upgradeDeck(deck, version): - "Upgrade deck to the latest version." - if version >= CURRENT_VERSION: - return - if version < 100: - _postSchemaUpgrade(deck) + # optimize and finish + deck.db.commit() + deck.db.execute("vacuum") + deck.db.execute("analyze") + deck.db.execute("update deck set ver = ?", SCHEMA_VERSION) + deck.save() diff --git a/anki/storage.py b/anki/storage.py index 40e30b666..3b795f11f 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -2,13 +2,12 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -CURRENT_VERSION = 1 - import os, simplejson from anki.lang import _ from anki.utils import intTime from anki.db import DB from anki.deck import _Deck +from anki.consts import * from anki.stdmodels import addBasicModel, addClozeModel def Deck(path, queue=True, lock=True, server=False): @@ -30,7 +29,7 @@ def Deck(path, queue=True, lock=True, server=False): db.execute("pragma cache_size = 10000") # add db to deck and do any remaining upgrades deck = _Deck(db, server) - if ver < CURRENT_VERSION: + if ver < SCHEMA_VERSION: _upgradeDeck(deck, ver) elif create: # add in reverse order so basic is default @@ -47,7 +46,7 @@ def Deck(path, queue=True, lock=True, server=False): # no upgrades necessary at the moment def _upgradeSchema(db): - return CURRENT_VERSION + return SCHEMA_VERSION def _upgradeDeck(deck, ver): return @@ -61,7 +60,7 @@ def _createDB(db): _addSchema(db) _updateIndices(db) db.execute("analyze") - return CURRENT_VERSION + return SCHEMA_VERSION def _addSchema(db, setDeckConf=True): db.executescript(""" @@ -140,7 +139,7 @@ create table if not exists graves ( insert or ignore into deck values(1,0,0,0,%(v)s,0,0,0,'','{}','','','{}'); -""" % ({'v':CURRENT_VERSION})) +""" % ({'v':SCHEMA_VERSION})) import anki.deck if setDeckConf: _addDeckVars(db, *_getDeckVars(db)) diff --git a/tests/test_deck.py b/tests/test_deck.py index 63047b31d..8f5e88c29 100644 --- a/tests/test_deck.py +++ b/tests/test_deck.py @@ -116,19 +116,6 @@ def test_fieldChecksum(): assert deck.db.scalar( "select count() from fsums") == 2 -def test_upgrade(): - dst = getUpgradeDeckPath() - print "upgrade to", dst - deck = Deck(dst) - # creation time should have been adjusted - d = datetime.datetime.fromtimestamp(deck.crt) - assert d.hour == 4 and d.minute == 0 - # 3 new, 2 failed, 1 due - deck.conf['counts'] = COUNT_REMAINING - assert deck.sched.cardCounts() == (3,2,1) - # now's a good time to test the integrity check too - deck.fixIntegrity() - def test_selective(): deck = getEmptyDeck() f = deck.newFact() diff --git a/tests/test_migration.py b/tests/test_migration.py index adedf0402..2d36586cc 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -1,7 +1,10 @@ # coding: utf-8 +import datetime +from anki.consts import * from shared import getUpgradeDeckPath from anki.migration.checker import check +from anki.migration.upgrader import Upgrader def test_checker(): dst = getUpgradeDeckPath() @@ -10,3 +13,20 @@ def test_checker(): open(dst, "w+").write("foo") assert not check(dst) +def test_upgrade(): + dst = getUpgradeDeckPath() + u = Upgrader() + print "upgrade to", dst + deck = u.upgrade(dst) + # creation time should have been adjusted + d = datetime.datetime.fromtimestamp(deck.crt) + assert d.hour == 4 and d.minute == 0 + # 3 new, 2 failed, 1 due + deck.reset() + deck.conf['counts'] = COUNT_REMAINING + assert deck.sched.cardCounts() == (3,2,1) + # now's a good time to test the integrity check too + deck.fixIntegrity() + + +