0cce540d83
To avoid all sorts of problems, we need to ensure cards scheduled with the V2 scheduler are not studied in older clients. Unfortunately we can't just bump the file's schema version, as the existing clients will freely import files created with newer versions. This patch changes that, so things should be a bit easier in the future. In the mean time, we need a way to prevent older clients from importing files created with the V2 scheduler. To do this, we switch to using a 'collection.anki21' file in the archive, and include a dummy collection .anki2 file. The code has been tested with both deck and collection packages, but exporting deck packages w/ scheduling info will remain disabled until the V2 scheduler has had more testing.
329 lines
11 KiB
Python
329 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import copy
|
|
import re
|
|
|
|
from anki.lang import _
|
|
from anki.utils import intTime, json
|
|
from anki.db import DB
|
|
from anki.collection import _Collection
|
|
from anki.consts import *
|
|
from anki.stdmodels import addBasicModel, addClozeModel, addForwardReverse, \
|
|
addForwardOptionalReverse, addBasicTypingModel
|
|
|
|
def Collection(path, lock=True, server=False, sync=True, log=False):
|
|
"Open a new or existing collection. Path must be unicode."
|
|
assert path.endswith(".anki2")
|
|
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)
|
|
db.setAutocommit(True)
|
|
if create:
|
|
ver = _createDB(db)
|
|
else:
|
|
ver = _upgradeSchema(db)
|
|
db.execute("pragma temp_store = memory")
|
|
if sync:
|
|
db.execute("pragma cache_size = 10000")
|
|
db.execute("pragma journal_mode = wal")
|
|
else:
|
|
db.execute("pragma synchronous = off")
|
|
db.setAutocommit(False)
|
|
# add db to col and do any remaining upgrades
|
|
col = _Collection(db, server, log)
|
|
if ver < SCHEMA_VERSION:
|
|
_upgrade(col, ver)
|
|
elif ver > SCHEMA_VERSION:
|
|
raise Exception("This file requires a newer version of Anki.")
|
|
elif create:
|
|
# add in reverse order so basic is default
|
|
addClozeModel(col)
|
|
addBasicTypingModel(col)
|
|
addForwardOptionalReverse(col)
|
|
addForwardReverse(col)
|
|
addBasicModel(col)
|
|
col.save()
|
|
if lock:
|
|
col.lock()
|
|
return col
|
|
|
|
def _upgradeSchema(db):
|
|
ver = db.scalar("select ver from col")
|
|
if ver == SCHEMA_VERSION:
|
|
return ver
|
|
# add odid to cards, edue->odue
|
|
######################################################################
|
|
if db.scalar("select ver from col") == 1:
|
|
db.execute("alter table cards rename to cards2")
|
|
_addSchema(db, setColConf=False)
|
|
db.execute("""
|
|
insert into cards select
|
|
id, nid, did, ord, mod, usn, type, queue, due, ivl, factor, reps, lapses,
|
|
left, edue, 0, flags, data from cards2""")
|
|
db.execute("drop table cards2")
|
|
db.execute("update col set ver = 2")
|
|
_updateIndices(db)
|
|
# remove did from notes
|
|
######################################################################
|
|
if db.scalar("select ver from col") == 2:
|
|
db.execute("alter table notes rename to notes2")
|
|
_addSchema(db, setColConf=False)
|
|
db.execute("""
|
|
insert into notes select
|
|
id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data from notes2""")
|
|
db.execute("drop table notes2")
|
|
db.execute("update col set ver = 3")
|
|
_updateIndices(db)
|
|
return ver
|
|
|
|
def _upgrade(col, ver):
|
|
if ver < 3:
|
|
# new deck properties
|
|
for d in col.decks.all():
|
|
d['dyn'] = 0
|
|
d['collapsed'] = False
|
|
col.decks.save(d)
|
|
if ver < 4:
|
|
col.modSchema(check=False)
|
|
clozes = []
|
|
for m in col.models.all():
|
|
if not "{{cloze:" in m['tmpls'][0]['qfmt']:
|
|
m['type'] = MODEL_STD
|
|
col.models.save(m)
|
|
else:
|
|
clozes.append(m)
|
|
for m in clozes:
|
|
_upgradeClozeModel(col, m)
|
|
col.db.execute("update col set ver = 4")
|
|
if ver < 5:
|
|
col.db.execute("update cards set odue = 0 where queue = 2")
|
|
col.db.execute("update col set ver = 5")
|
|
if ver < 6:
|
|
col.modSchema(check=False)
|
|
import anki.models
|
|
for m in col.models.all():
|
|
m['css'] = anki.models.defaultModel['css']
|
|
for t in m['tmpls']:
|
|
if 'css' not in t:
|
|
# ankidroid didn't bump version
|
|
continue
|
|
m['css'] += "\n" + t['css'].replace(
|
|
".card ", ".card%d "%(t['ord']+1))
|
|
del t['css']
|
|
col.models.save(m)
|
|
col.db.execute("update col set ver = 6")
|
|
if ver < 7:
|
|
col.modSchema(check=False)
|
|
col.db.execute(
|
|
"update cards set odue = 0 where (type = 1 or queue = 2) "
|
|
"and not odid")
|
|
col.db.execute("update col set ver = 7")
|
|
if ver < 8:
|
|
col.modSchema(check=False)
|
|
col.db.execute(
|
|
"update cards set due = due / 1000 where due > 4294967296")
|
|
col.db.execute("update col set ver = 8")
|
|
if ver < 9:
|
|
# adding an empty file to a zip makes python's zip code think it's a
|
|
# folder, so remove any empty files
|
|
changed = False
|
|
dir = col.media.dir()
|
|
if dir:
|
|
for f in os.listdir(col.media.dir()):
|
|
if os.path.isfile(f) and not os.path.getsize(f):
|
|
os.unlink(f)
|
|
col.media.db.execute(
|
|
"delete from log where fname = ?", f)
|
|
col.media.db.execute(
|
|
"delete from media where fname = ?", f)
|
|
changed = True
|
|
if changed:
|
|
col.media.db.commit()
|
|
col.db.execute("update col set ver = 9")
|
|
if ver < 10:
|
|
col.db.execute("""
|
|
update cards set left = left + left*1000 where queue = 1""")
|
|
col.db.execute("update col set ver = 10")
|
|
if ver < 11:
|
|
col.modSchema(check=False)
|
|
for d in col.decks.all():
|
|
if d['dyn']:
|
|
order = d['order']
|
|
# failed order was removed
|
|
if order >= 5:
|
|
order -= 1
|
|
d['terms'] = [[d['search'], d['limit'], order]]
|
|
del d['search']
|
|
del d['limit']
|
|
del d['order']
|
|
d['resched'] = True
|
|
d['return'] = True
|
|
else:
|
|
if 'extendNew' not in d:
|
|
d['extendNew'] = 10
|
|
d['extendRev'] = 50
|
|
col.decks.save(d)
|
|
for c in col.decks.allConf():
|
|
r = c['rev']
|
|
r['ivlFct'] = r.get("ivlfct", 1)
|
|
if 'ivlfct' in r:
|
|
del r['ivlfct']
|
|
r['maxIvl'] = 36500
|
|
col.decks.save(c)
|
|
for m in col.models.all():
|
|
for t in m['tmpls']:
|
|
t['bqfmt'] = ''
|
|
t['bafmt'] = ''
|
|
col.models.save(m)
|
|
col.db.execute("update col set ver = 11")
|
|
|
|
def _upgradeClozeModel(col, m):
|
|
m['type'] = MODEL_CLOZE
|
|
# convert first template
|
|
t = m['tmpls'][0]
|
|
for type in 'qfmt', 'afmt':
|
|
t[type] = re.sub("{{cloze:1:(.+?)}}", r"{{cloze:\1}}", t[type])
|
|
t['name'] = _("Cloze")
|
|
# delete non-cloze cards for the model
|
|
rem = []
|
|
for t in m['tmpls'][1:]:
|
|
if "{{cloze:" not in t['qfmt']:
|
|
rem.append(t)
|
|
for r in rem:
|
|
col.models.remTemplate(m, r)
|
|
del m['tmpls'][1:]
|
|
col.models._updateTemplOrds(m)
|
|
col.models.save(m)
|
|
|
|
# Creating a new collection
|
|
######################################################################
|
|
|
|
def _createDB(db):
|
|
db.execute("pragma page_size = 4096")
|
|
db.execute("pragma legacy_file_format = 0")
|
|
db.execute("vacuum")
|
|
_addSchema(db)
|
|
_updateIndices(db)
|
|
db.execute("analyze")
|
|
return SCHEMA_VERSION
|
|
|
|
def _addSchema(db, setColConf=True):
|
|
db.executescript("""
|
|
create table if not exists col (
|
|
id integer primary key,
|
|
crt integer not null,
|
|
mod integer not null,
|
|
scm integer not null,
|
|
ver integer not null,
|
|
dty integer not null,
|
|
usn integer not null,
|
|
ls integer not null,
|
|
conf text not null,
|
|
models text not null,
|
|
decks text not null,
|
|
dconf text not null,
|
|
tags text not null
|
|
);
|
|
|
|
create table if not exists notes (
|
|
id integer primary key, /* 0 */
|
|
guid text not null, /* 1 */
|
|
mid integer not null, /* 2 */
|
|
mod integer not null, /* 3 */
|
|
usn integer not null, /* 4 */
|
|
tags text not null, /* 5 */
|
|
flds text not null, /* 6 */
|
|
sfld integer not null, /* 7 */
|
|
csum integer not null, /* 8 */
|
|
flags integer not null, /* 9 */
|
|
data text not null /* 10 */
|
|
);
|
|
|
|
create table if not exists cards (
|
|
id integer primary key, /* 0 */
|
|
nid integer not null, /* 1 */
|
|
did integer not null, /* 2 */
|
|
ord integer not null, /* 3 */
|
|
mod integer not null, /* 4 */
|
|
usn integer not null, /* 5 */
|
|
type integer not null, /* 6 */
|
|
queue integer not null, /* 7 */
|
|
due integer not null, /* 8 */
|
|
ivl integer not null, /* 9 */
|
|
factor integer not null, /* 10 */
|
|
reps integer not null, /* 11 */
|
|
lapses integer not null, /* 12 */
|
|
left integer not null, /* 13 */
|
|
odue integer not null, /* 14 */
|
|
odid integer not null, /* 15 */
|
|
flags integer not null, /* 16 */
|
|
data text not null /* 17 */
|
|
);
|
|
|
|
create table if not exists revlog (
|
|
id integer primary key,
|
|
cid integer not null,
|
|
usn integer not null,
|
|
ease integer not null,
|
|
ivl integer not null,
|
|
lastIvl integer not null,
|
|
factor integer not null,
|
|
time integer not null,
|
|
type integer not null
|
|
);
|
|
|
|
create table if not exists graves (
|
|
usn integer not null,
|
|
oid integer not null,
|
|
type integer not null
|
|
);
|
|
|
|
insert or ignore into col
|
|
values(1,0,0,%(s)s,%(v)s,0,0,0,'','{}','','','{}');
|
|
""" % ({'v':SCHEMA_VERSION, 's':intTime(1000)}))
|
|
if setColConf:
|
|
_addColVars(db, *_getColVars(db))
|
|
|
|
def _getColVars(db):
|
|
import anki.collection
|
|
import anki.decks
|
|
g = copy.deepcopy(anki.decks.defaultDeck)
|
|
g['id'] = 1
|
|
g['name'] = _("Default")
|
|
g['conf'] = 1
|
|
g['mod'] = intTime()
|
|
gc = copy.deepcopy(anki.decks.defaultConf)
|
|
gc['id'] = 1
|
|
return g, gc, anki.collection.defaultConf.copy()
|
|
|
|
def _addColVars(db, g, gc, c):
|
|
db.execute("""
|
|
update col set conf = ?, decks = ?, dconf = ?""",
|
|
json.dumps(c),
|
|
json.dumps({'1': g}),
|
|
json.dumps({'1': gc}))
|
|
|
|
def _updateIndices(db):
|
|
"Add indices to the DB."
|
|
db.executescript("""
|
|
-- syncing
|
|
create index if not exists ix_notes_usn on notes (usn);
|
|
create index if not exists ix_cards_usn on cards (usn);
|
|
create index if not exists ix_revlog_usn on revlog (usn);
|
|
-- card spacing, etc
|
|
create index if not exists ix_cards_nid on cards (nid);
|
|
-- scheduling and deck limiting
|
|
create index if not exists ix_cards_sched on cards (did, queue, due);
|
|
-- revlog by card
|
|
create index if not exists ix_revlog_cid on revlog (cid);
|
|
-- field uniqueness
|
|
create index if not exists ix_notes_csum on notes (csum);
|
|
""")
|