move models into the deck table

Like the previous change, models have been moved from a separate DB table to
an entry in the deck. We need them for many operations including reviewing,
and it's easier to keep them in memory than half on disk with a cache that
gets cleared every time we .reset(). This means they are easily serialized as
well - previously they were part Python and part JSON, which made access
confusing.

Because the data is all pulled from JSON now, the instance methods have been
moved to the model registry. Eg:
  model.addField(...) -> deck.models.addField(model, ...).

- IDs are now timestamped as with groups et al.

- The data field for plugins was also removed. Config info can be added to
  deck.conf; larger data should be stored externally.

- Upgrading needs to be updated for the new model structure.

- HexifyID() now accepts strings as well, as our IDs get converted to strings
  in the serialization process.
This commit is contained in:
Damien Elmes 2011-08-27 22:27:09 +09:00
parent 7afe6a9a7d
commit d3a3edb707
19 changed files with 388 additions and 390 deletions

View file

@ -105,9 +105,9 @@ lapses=?, grade=?, cycles=?, edue=? where id = ?""",
def _getQA(self, reload=False):
if not self._qa or reload:
f = self.fact(); m = self.model()
data = [self.id, f.id, m.id, self.gid, self.ord, f.stringTags(),
data = [self.id, f.id, m['id'], self.gid, self.ord, f.stringTags(),
f.joinedFields()]
self._qa = self.deck._renderQA(self.model(), data)
self._qa = self.deck._renderQA(data)
return self._qa
def _withClass(self, txt, extra):
@ -117,7 +117,7 @@ lapses=?, grade=?, cycles=?, edue=? where id = ?""",
"Fetch the model and fact."
if not self._rd or reload:
f = self.deck.getFact(self.fid)
m = self.deck.getModel(f.mid)
m = self.deck.models.get(f.mid)
self._rd = [f, m]
return self._rd
@ -128,10 +128,10 @@ lapses=?, grade=?, cycles=?, edue=? where id = ?""",
return self._reviewData()[1]
def template(self):
return self._reviewData()[1].templates[self.ord]
return self._reviewData()[1]['tmpls'][self.ord]
def cssClass(self):
return "cm%s-%s" % (hexifyID(self.model().id),
return "cm%s-%s" % (hexifyID(self.model()['id']),
hexifyID(self.template()['ord']))
def startTimer(self):

View file

@ -9,12 +9,13 @@ from anki.utils import parseTags, ids2str, hexifyID, \
splitFields
from anki.hooks import runHook, runFilter
from anki.sched import Scheduler
from anki.models import ModelRegistry
from anki.media import MediaRegistry
from anki.consts import *
from anki.errors import AnkiError
import anki.latex # sets up hook
import anki.cards, anki.facts, anki.models, anki.template, anki.cram, \
import anki.cards, anki.facts, anki.template, anki.cram, \
anki.groups, anki.find
# Settings related to queue building. These may be loaded without the rest of
@ -51,6 +52,8 @@ class _Deck(object):
self.path = db._path
self._lastSave = time.time()
self.clearUndo()
self.media = MediaRegistry(self)
self.models = ModelRegistry(self)
self.load()
if not self.crt:
d = datetime.datetime.today()
@ -65,7 +68,6 @@ class _Deck(object):
self.lastSessionStart = 0
self._stdSched = Scheduler(self)
self.sched = self._stdSched
self.media = MediaRegistry(self)
# check for improper shutdown
self.cleanup()
@ -85,16 +87,16 @@ class _Deck(object):
self.lastSync,
self.qconf,
self.conf,
models,
self.groups,
self.gconf,
self.data) = self.db.first("""
self.gconf) = self.db.first("""
select crt, mod, scm, dty, syncName, lastSync,
qconf, conf, groups, gconf, data from deck""")
qconf, conf, models, groups, gconf from deck""")
self.qconf = simplejson.loads(self.qconf)
self.conf = simplejson.loads(self.conf)
self.groups = simplejson.loads(self.groups)
self.gconf = simplejson.loads(self.gconf)
self.data = simplejson.loads(self.data)
self.models.load(models)
def flush(self, mod=None):
"Flush state to DB, updating mod time."
@ -102,11 +104,14 @@ qconf, conf, groups, gconf, data from deck""")
self.db.execute(
"""update deck set
crt=?, mod=?, scm=?, dty=?, syncName=?, lastSync=?,
qconf=?, conf=?, data=?""",
qconf=?, conf=?, groups=?, gconf=?""",
self.crt, self.mod, self.scm, self.dty,
self.syncName, self.lastSync,
simplejson.dumps(self.qconf),
simplejson.dumps(self.conf), simplejson.dumps(self.data))
simplejson.dumps(self.conf),
simplejson.dumps(self.groups),
simplejson.dumps(self.gconf))
self.models.flush()
def save(self, name=None, mod=None):
"Flush, commit DB, and take out another write lock."
@ -187,15 +192,6 @@ qconf=?, conf=?, data=?""",
def getFact(self, id):
return anki.facts.Fact(self, id=id)
def getModel(self, mid, cache=True):
"Memoizes; call .reset() to reset cache."
if cache and mid in self.modelCache:
return self.modelCache[mid]
m = anki.models.Model(self, mid)
if cache:
self.modelCache[mid] = m
return m
# Utils
##########################################################################
@ -227,7 +223,7 @@ qconf=?, conf=?, data=?""",
def newFact(self):
"Return a new fact with the current model."
return anki.facts.Fact(self, self.currentModel())
return anki.facts.Fact(self, self.models.current())
def addFact(self, fact):
"Add a fact to the deck. Return number of new cards."
@ -273,14 +269,14 @@ qconf=?, conf=?, data=?""",
"Return (active), non-empty templates."
ok = []
model = fact.model()
for template in model.templates:
for template in model['tmpls']:
if template['actv'] or not checkActive:
# [cid, fid, mid, gid, ord, tags, flds]
data = [1, 1, model.id, 1, template['ord'],
data = [1, 1, model['id'], 1, template['ord'],
"", fact.joinedFields()]
now = self._renderQA(model, data)
now = self._renderQA(data)
data[6] = "\x1f".join([""]*len(fact.fields))
empty = self._renderQA(model, data)
empty = self._renderQA(data)
if now['q'] == empty['q']:
continue
if not template['emptyAns']:
@ -321,7 +317,7 @@ qconf=?, conf=?, data=?""",
elif type == 1:
cms = [c.template() for c in fact.cards()]
else:
cms = fact.model().templates
cms = fact.model()['tmpls']
if not cms:
return []
cards = []
@ -365,45 +361,6 @@ select id from facts where id in %s and id not in (select fid from cards)""" %
ids2str(fids))
self._delFacts(fids)
# Models
##########################################################################
def currentModel(self):
return self.getModel(self.conf['currentModelId'])
def models(self):
"Return a dict of mid -> model."
mods = {}
for m in [self.getModel(id) for id in self.db.list(
"select id from models")]:
mods[m.id] = m
return mods
def addModel(self, model):
self.modSchema()
model.flush()
self.conf['currentModelId'] = model.id
def delModel(self, mid):
"Delete MODEL, and all its cards/facts."
self.modSchema()
# delete facts/cards
self.delCards(self.db.list("""
select id from cards where fid in (select id from facts where mid = ?)""",
mid))
# then the model
self.db.execute("delete from models where id = ?", mid)
# GUI should ensure last model is not deleted
if self.conf['currentModelId'] == mid:
self.conf['currentModelId'] = self.db.scalar(
"select id from models limit 1")
def allCSS(self):
return "\n".join(self.db.list("select css from models"))
def modelId(self, name):
return self.db.scalar("select id from models where name = ?", name)
# Field checksums and sorting fields
##########################################################################
@ -414,17 +371,16 @@ select id from cards where fid in (select id from facts where mid = ?)""",
def updateFieldCache(self, fids, csum=True):
"Update field checksums and sort cache, after find&replace, etc."
sfids = ids2str(fids)
mods = self.models()
r = []
r2 = []
for (fid, mid, flds) in self._fieldData(sfids):
fields = splitFields(flds)
model = mods[mid]
model = self.models.get(mid)
if csum:
for f in model.fields:
for f in model['flds']:
if f['uniq'] and fields[f['ord']]:
r.append((fid, mid, fieldChecksum(fields[f['ord']])))
r2.append((stripHTML(fields[model.sortIdx()]), fid))
r2.append((stripHTML(fields[self.models.sortIdx(model)]), fid))
if csum:
self.db.execute("delete from fsums where fid in "+sfids)
self.db.executemany("insert into fsums values (?,?,?)", r)
@ -445,17 +401,17 @@ select id from cards where fid in (select id from facts where mid = ?)""",
where = ""
else:
raise Exception()
mods = self.models()
return [self._renderQA(mods[row[2]], row)
return [self._renderQA(row)
for row in self._qaData(where)]
def _renderQA(self, model, data):
def _renderQA(self, data):
"Returns hash of id, question, answer."
# data is [cid, fid, mid, gid, ord, tags, flds]
# unpack fields and create dict
flist = splitFields(data[6])
fields = {}
for (name, (idx, conf)) in model.fieldMap().items():
model = self.models.get(data[2])
for (name, (idx, conf)) in self.models.fieldMap(model).items():
fields[name] = flist[idx]
if fields[name]:
fields[name] = '<span class="fm%s-%s">%s</span>' % (
@ -463,9 +419,9 @@ select id from cards where fid in (select id from facts where mid = ?)""",
else:
fields[name] = ""
fields['Tags'] = data[5]
fields['Model'] = model.name
fields['Model'] = model['name']
fields['Group'] = self.groupName(data[3])
template = model.templates[data[4]]
template = model['tmpls'][data[4]]
fields['Template'] = template['name']
# render q & a
d = dict(id=data[0])
@ -473,7 +429,7 @@ select id from cards where fid in (select id from facts where mid = ?)""",
if type == "q":
format = format.replace("cloze:", "cq:")
else:
if model.conf['clozectx']:
if model['clozectx']:
name = "cactx:"
else:
name = "ca:"
@ -770,8 +726,8 @@ select id from facts where id not in (select distinct fid from cards)""")
self.db.execute("delete from tags")
self.updateFactTags()
# field cache
for m in self.models().values():
self.updateFieldCache(m.fids())
for m in self.models.all():
self.updateFieldCache(self.models.fids(m['id']))
# and finally, optimize
self.optimize()
newSize = os.stat(self.path)[stat.ST_SIZE]

View file

@ -19,12 +19,12 @@ class Fact(object):
else:
self.id = timestampID(deck.db, "facts")
self._model = model
self.gid = model.conf['gid']
self.mid = model.id
self.gid = model['gid']
self.mid = model['id']
self.tags = []
self.fields = [""] * len(self._model.fields)
self.fields = [""] * len(self._model['flds'])
self.data = ""
self._fmap = self._model.fieldMap()
self._fmap = self.deck.models.fieldMap(self._model)
def load(self):
(self.mid,
@ -36,12 +36,12 @@ class Fact(object):
select mid, gid, mod, tags, flds, data from facts where id = ?""", self.id)
self.fields = splitFields(self.fields)
self.tags = parseTags(self.tags)
self._model = self.deck.getModel(self.mid)
self._fmap = self._model.fieldMap()
self._model = self.deck.models.get(self.mid)
self._fmap = self.deck.models.fieldMap(self._model)
def flush(self):
self.mod = intTime()
sfld = stripHTML(self.fields[self._model.sortIdx()])
sfld = stripHTML(self.fields[self.deck.models.sortIdx(self._model)])
tags = self.stringTags()
res = self.deck.db.execute("""
insert or replace into facts values (?, ?, ?, ?, ?, ?, ?, ?)""",

View file

@ -20,8 +20,8 @@ SEARCH_GROUP = 7
def fieldNames(deck, downcase=True):
fields = set()
names = []
for m in deck.models().values():
for f in m.fields:
for m in deck.models.all():
for f in m['flds']:
if f['name'].lower() not in fields:
names.append(f['name'])
fields.add(f['name'].lower())
@ -119,7 +119,7 @@ order by %s""" % (lim, sort)
elif type == SEARCH_FIELD:
self._findField(token, isNeg)
elif type == SEARCH_MODEL:
self._findModel(token, isNeg, c)
self._findModel(token, isNeg)
elif type == SEARCH_GROUP:
self._findGroup(token, isNeg)
else:
@ -182,12 +182,13 @@ order by %s""" % (lim, sort)
def _findFids(self, val):
self.lims['fact'].append("id in (%s)" % val)
def _findModel(self, val, isNeg, c):
def _findModel(self, val, isNeg):
extra = "not" if isNeg else ""
self.lims['fact'].append(
"mid %s in (select id from models where name like :_mod_%d)" % (
extra, c))
self.lims['args']['_mod_%d'%c] = val
ids = []
for m in self.deck.models.all():
if m['name'].lower() == val:
ids.append(m['id'])
self.lims['fact'].append("mid %s in %s" % (extra, ids2str(ids)))
def _findGroup(self, val, isNeg):
extra = "!" if isNeg else ""
@ -203,8 +204,8 @@ order by %s""" % (lim, sort)
except:
num = None
lims = []
for m in self.deck.models().values():
for t in m.templates:
for m in self.deck.models.all():
for t in m['tmpls']:
# ordinal number?
if num is not None and t['ord'] == num:
self.lims['card'].append("ord %s %d" % (comp, num))
@ -212,8 +213,8 @@ order by %s""" % (lim, sort)
# template name?
elif t['name'].lower() == val.lower():
lims.append((
"(fid in (select id from facts where mid = %d) "
"and ord %s %d)") % (m.id, comp, t['ord']))
"(fid in (select id from facts where mid = %s) "
"and ord %s %d)") % (m['id'], comp, t['ord']))
found = True
if lims:
self.lims['card'].append("(" + " or ".join(lims) + ")")
@ -226,10 +227,10 @@ order by %s""" % (lim, sort)
value = "%" + parts[1].replace("*", "%") + "%"
# find models that have that field
mods = {}
for m in self.deck.models().values():
for f in m.fields:
for m in self.deck.models.all():
for f in m['flds']:
if f['name'].lower() == field:
mods[m.id] = (m, f['ord'])
mods[m['id']] = (m, f['ord'])
if not mods:
# nothing has that field
self.lims['valid'] = False
@ -243,11 +244,11 @@ where mid in %s and flds like ? escape '\\'""" % (
ids2str(mods.keys())),
"%" if self.full else value):
flds = splitFields(flds)
ord = mods[mid][1]
str = flds[ord]
ord = mods[str(mid)][1]
strg = flds[ord]
if self.full:
str = stripHTML(str)
if re.search(regex, str):
strg = stripHTML(strg)
if re.search(regex, strg):
fids.append(id)
extra = "not" if isNeg else ""
self.lims['fact'].append("id %s in %s" % (extra, ids2str(fids)))
@ -372,10 +373,10 @@ def findReplace(deck, fids, src, dst, regex=False, field=None, fold=True):
"Find and replace fields in a fact."
mmap = {}
if field:
for m in deck.models().values():
for f in m.fields:
for m in deck.models.all():
for f in m['flds']:
if f['name'] == field:
mmap[m.id] = f['ord']
mmap[m['id']] = f['ord']
if not mmap:
return 0
# find and gather replacements
@ -393,7 +394,7 @@ def findReplace(deck, fids, src, dst, regex=False, field=None, fold=True):
# does it match?
sflds = splitFields(flds)
if field:
ord = mmap[mid]
ord = mmap[str(mid)]
sflds[ord] = repl(sflds[ord])
else:
for c in range(len(sflds)):

View file

@ -71,9 +71,9 @@ def _latexFromHtml(deck, latex):
def _buildImg(deck, latex, fname, model):
# add header/footer
latex = (model.conf["latexPre"] + "\n" +
latex = (model["latexPre"] + "\n" +
latex + "\n" +
model.conf["latexPost"])
model["latexPost"])
# write into a temp file
log = open(namedtmp("latex_log.txt"), "w")
texfile = file(namedtmp("tmp.tex"), "w")

View file

@ -2,19 +2,19 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import simplejson
import simplejson, copy
from anki.utils import intTime, hexifyID, joinFields, splitFields, ids2str, \
timestampID
from anki.lang import _
# Models
##########################################################################
# gid may point to non-existent group
defaultConf = {
# careful not to add any lists/dicts/etc here, as they aren't deep copied
defaultModel = {
'css': "",
'sortf': 0,
'gid': 1,
'tags': [],
'clozectx': False,
'latexPre': """\
\\documentclass[12pt]{article}
@ -56,82 +56,132 @@ defaultTemplate = {
'gid': None,
}
class Model(object):
class ModelRegistry(object):
def __init__(self, deck, id=None):
# Saving/loading registry
#############################################################
def __init__(self, deck):
self.deck = deck
if id:
self.id = id
self.load()
else:
self.id = timestampID(deck.db, "models")
self.name = u""
self.conf = defaultConf.copy()
self.css = ""
self.fields = []
self.templates = []
def load(self):
(self.mod,
self.name,
self.fields,
self.templates,
self.conf,
self.css) = self.deck.db.first("""
select mod, name, flds, tmpls, conf, css from models where id = ?""", self.id)
self.fields = simplejson.loads(self.fields)
self.templates = simplejson.loads(self.templates)
self.conf = simplejson.loads(self.conf)
def load(self, json):
"Load registry from JSON."
self.changed = False
self.models = simplejson.loads(json)
def save(self, m=None):
"Mark M modified if provided, and schedule registry flush."
if m:
m['mod'] = intTime()
m['css'] = self._css(m)
self.changed = True
def flush(self):
self.mod = intTime()
self.css = self.genCSS()
ret = self.deck.db.execute("""
insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""",
self.id, self.mod, self.name,
simplejson.dumps(self.fields),
simplejson.dumps(self.templates),
simplejson.dumps(self.conf),
self.css)
self.id = ret.lastrowid
"Flush the registry if any models were changed."
if self.changed:
self.deck.db.execute("update deck set models = ?",
simplejson.dumps(self.models))
def fids(self):
# Retrieving and creating models
#############################################################
def current(self):
"Get current model."
return self.get(self.deck.conf['currentModelId'])
def get(self, id):
"Get model with ID."
return self.models[str(id)]
def all(self):
"Get all models."
return self.models.values()
def byName(self, name):
"Get model with NAME."
for m in self.models.values():
if m['name'].lower() == name.lower():
return m
def new(self, name):
"Create a new model, save it in the registry, and return it."
# caller should call save() after modifying
m = defaultModel.copy()
m['name'] = name
m['mod'] = intTime()
m['flds'] = []
m['tmpls'] = []
m['tags'] = []
return self._add(m)
def del_(self, m):
"Delete model, and all its cards/facts."
self.deck.modSchema()
# delete facts/cards
self.deck.delCards(self.deck.db.list("""
select id from cards where fid in (select id from facts where mid = ?)""",
m['id']))
# then the model
del self.models[m['id']]
self.save()
# GUI should ensure last model is not deleted
if self.deck.conf['currentModelId'] == m['id']:
self.deck.conf['currentModelId'] = int(self.models.keys()[0])
def _add(self, m):
self._setID(m)
self.models[m['id']] = m
self.save(m)
self.deck.conf['currentModelId'] = m['id']
return m
def _setID(self, m):
while 1:
id = str(intTime(1000))
if id not in self.models:
break
m['id'] = id
# Tools
##################################################
def fids(self, m):
"Fact ids for M."
return self.deck.db.list(
"select id from facts where mid = ?", self.id)
"select id from facts where mid = ?", m['id'])
def useCount(self):
def useCount(self, m):
"Number of fact using M."
return self.deck.db.scalar(
"select count() from facts where mid = ?", self.id)
"select count() from facts where mid = ?", m['id'])
def css(self):
"CSS for all models."
return "\n".join([m['css'] for m in self.all()])
# Copying
##################################################
def copy(self):
"Copy, flush and return."
new = Model(self.deck, self.id)
new.id = None
new.name += _(" copy")
new.fields = [f.copy() for f in self.fields]
new.templates = [t.copy() for t in self.templates]
new.flush()
return new
def copy(self, m):
"Copy, save and return."
m2 = copy.deepcopy(m)
m2['name'] = _("%s copy") % m2['name']
return self._add(m2)
# CSS generation
##################################################
def genCSS(self):
if not self.id:
return ""
def _css(self, m):
# fields
css = "".join(self._fieldCSS(
".fm%s-%s" % (hexifyID(self.id), hexifyID(f['ord'])),
".fm%s-%s" % (hexifyID(m['id']), hexifyID(f['ord'])),
(f['font'], f['qsize'], f['qcol'], f['rtl'], f['pre']))
for f in self.fields)
for f in m['flds'])
# templates
css += "".join(".cm%s-%s {text-align:%s;background:%s}\n" % (
hexifyID(self.id), hexifyID(t['ord']),
hexifyID(m['id']), hexifyID(t['ord']),
("center", "left", "right")[t['align']], t['bg'])
for t in self.templates)
for t in m['tmpls'])
return css
def _rewriteFont(self, font):
@ -158,64 +208,66 @@ insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""",
# Fields
##################################################
def fieldMap(self):
def newField(self, name):
f = defaultField.copy()
f['name'] = name
return f
def fieldMap(self, m):
"Mapping of field name -> (ord, field)."
return dict((f['name'], (f['ord'], f)) for f in self.fields)
return dict((f['name'], (f['ord'], f)) for f in m['flds'])
def sortIdx(self):
return self.conf['sortf']
def sortIdx(self, m):
return m['sortf']
def setSortIdx(self, idx):
assert idx >= 0 and idx < len(self.fields)
def setSortIdx(self, m, idx):
assert idx >= 0 and idx < len(m['flds'])
self.deck.modSchema()
self.conf['sortf'] = idx
self.deck.updateFieldCache(self.fids(), csum=False)
self.flush()
m['sortf'] = idx
self.deck.updateFieldCache(self.fids(m), csum=False)
self.save(m)
def newField(self):
return defaultField.copy()
def addField(self, field):
self.fields.append(field)
self._updateFieldOrds()
self.flush()
def addField(self, m, field):
m['flds'].append(field)
self._updateFieldOrds(m)
self.save(m)
def add(fields):
fields.append("")
return fields
self._transformFields(add)
self._transformFields(m, add)
def delField(self, field):
idx = self.fields.index(field)
self.fields.remove(field)
self._updateFieldOrds()
def delField(self, m, field):
idx = m['flds'].index(field)
m['flds'].remove(field)
self._updateFieldOrds(m)
def delete(fields):
del fields[idx]
return fields
self._transformFields(delete)
if idx == self.sortIdx():
self._transformFields(m, delete)
if idx == self.sortIdx(m):
# need to rebuild
self.deck.updateFieldCache(self.fids(), csum=False)
# flushes
self.renameField(field, None)
self.deck.updateFieldCache(self.fids(m), csum=False)
# saves
self.renameField(m, field, None)
def moveField(self, field, idx):
oldidx = self.fields.index(field)
def moveField(self, m, field, idx):
oldidx = m['flds'].index(field)
if oldidx == idx:
return
self.fields.remove(field)
self.fields.insert(idx, field)
self._updateFieldOrds()
self.flush()
m['flds'].remove(field)
m['flds'].insert(idx, field)
self._updateFieldOrds(m)
self.save(m)
def move(fields, oldidx=oldidx):
val = fields[oldidx]
del fields[oldidx]
fields.insert(idx, val)
return fields
self._transformFields(move)
self._transformFields(m, move)
def renameField(self, field, newName):
def renameField(self, m, field, newName):
self.deck.modSchema()
for t in self.templates:
for t in m['tmpls']:
types = ("{{%s}}", "{{text:%s}}", "{{#%s}}",
"{{^%s}}", "{{/%s}}")
for type in types:
@ -226,77 +278,79 @@ insert or replace into models values (?, ?, ?, ?, ?, ?, ?)""",
repl = ""
t[fmt] = t[fmt].replace(type%field['name'], repl)
field['name'] = newName
self.flush()
self.save(m)
def _updateFieldOrds(self):
for c, f in enumerate(self.fields):
def _updateFieldOrds(self, m):
for c, f in enumerate(m['flds']):
f['ord'] = c
def _transformFields(self, fn):
def _transformFields(self, m, fn):
self.deck.modSchema()
r = []
for (id, flds) in self.deck.db.execute(
"select id, flds from facts where mid = ?", self.id):
"select id, flds from facts where mid = ?", m['id']):
r.append((joinFields(fn(splitFields(flds))), id))
self.deck.db.executemany("update facts set flds = ? where id = ?", r)
# Templates
##################################################
def newTemplate(self):
return defaultTemplate.copy()
def newTemplate(self, name):
t = defaultTemplate.copy()
t['name'] = name
return t
def addTemplate(self, template):
def addTemplate(self, m, template):
self.deck.modSchema()
self.templates.append(template)
self._updateTemplOrds()
self.flush()
m['tmpls'].append(template)
self._updateTemplOrds(m)
self.save(m)
def delTemplate(self, template):
def delTemplate(self, m, template):
self.deck.modSchema()
ord = self.templates.index(template)
ord = m['tmpls'].index(template)
cids = self.deck.db.list("""
select c.id from cards c, facts f where c.fid=f.id and mid = ? and ord = ?""",
self.id, ord)
m['id'], ord)
self.deck.delCards(cids)
# shift ordinals
self.deck.db.execute("""
update cards set ord = ord - 1 where fid in (select id from facts
where mid = ?) and ord > ?""", self.id, ord)
self.templates.remove(template)
self._updateTemplOrds()
self.flush()
where mid = ?) and ord > ?""", m['id'], ord)
m['tmpls'].remove(template)
self._updateTemplOrds(m)
self.save(m)
def _updateTemplOrds(self):
for c, t in enumerate(self.templates):
def _updateTemplOrds(self, m):
for c, t in enumerate(m['tmpls']):
t['ord'] = c
def moveTemplate(self, template, idx):
oldidx = self.templates.index(template)
def moveTemplate(self, m, template, idx):
oldidx = m['tmpls'].index(template)
if oldidx == idx:
return
oldidxs = dict((id(t), t['ord']) for t in self.templates)
self.templates.remove(template)
self.templates.insert(idx, template)
self._updateTemplOrds()
oldidxs = dict((id(t), t['ord']) for t in m['tmpls'])
m['tmpls'].remove(template)
m['tmpls'].insert(idx, template)
self._updateTemplOrds(m)
# generate change map
map = []
for t in self.templates:
for t in m['tmpls']:
map.append("when ord = %d then %d" % (oldidxs[id(t)], t['ord']))
# apply
self.flush()
self.save(m)
self.deck.db.execute("""
update cards set ord = (case %s end) where fid in (
select id from facts where mid = ?)""" % " ".join(map), self.id)
select id from facts where mid = ?)""" % " ".join(map), m['id'])
# Model changing
##########################################################################
# - maps are ord->ord, and there should not be duplicate targets
# - newModel should be self if model is not changing
def changeModel(self, fids, newModel, fmap, cmap):
def change(self, m, fids, newModel, fmap, cmap):
self.deck.modSchema()
assert newModel.id == self.id or (fmap and cmap)
assert newModel['id'] == m['id'] or (fmap and cmap)
if fmap:
self._changeFacts(fids, newModel, fmap)
if cmap:
@ -304,7 +358,7 @@ select id from facts where mid = ?)""" % " ".join(map), self.id)
def _changeFacts(self, fids, newModel, map):
d = []
nfields = len(newModel.fields)
nfields = len(newModel['flds'])
for (fid, flds) in self.deck.db.execute(
"select id, flds from facts where id in "+ids2str(fids)):
newflds = {}
@ -315,7 +369,7 @@ select id from facts where mid = ?)""" % " ".join(map), self.id)
for c in range(nfields):
flds.append(newflds.get(c, ""))
flds = joinFields(flds)
d.append(dict(fid=fid, flds=flds, mid=newModel.id))
d.append(dict(fid=fid, flds=flds, mid=newModel['id']))
self.deck.db.executemany(
"update facts set flds=:flds, mid=:mid where id = :fid", d)
self.deck.updateFieldCache(fids)

View file

@ -47,7 +47,7 @@ class CardStats(object):
self.addLine(_("Total Time"), self.time(total))
elif c.queue == 0:
self.addLine(_("Position"), c.due)
self.addLine(_("Model"), c.model().name)
self.addLine(_("Model"), c.model()['name'])
self.addLine(_("Template"), c.template()['name'])
self.addLine(_("Current Group"), self.deck.groupName(c.gid))
self.addLine(_("Initial Group"), self.deck.groupName(c.fact().gid))

View file

@ -2,7 +2,6 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from anki.models import Model
from anki.lang import _
models = []
@ -10,55 +9,50 @@ models = []
# Basic
##########################################################################
def BasicModel(deck):
m = Model(deck)
m.name = _("Basic")
fm = m.newField()
fm['name'] = _("Front")
def addBasicModel(deck):
mm = deck.models
m = mm.new(_("Basic"))
fm = mm.newField(_("Front"))
fm['req'] = True
fm['uniq'] = True
m.addField(fm)
fm = m.newField()
fm['name'] = _("Back")
m.addField(fm)
t = m.newTemplate()
t['name'] = _("Forward")
mm.addField(m, fm)
fm = mm.newField(_("Back"))
mm.addField(m, fm)
t = mm.newTemplate(_("Forward"))
t['qfmt'] = "{{" + _("Front") + "}}"
t['afmt'] = "{{" + _("Back") + "}}"
m.addTemplate(t)
t = m.newTemplate()
t['name'] = _("Reverse")
mm.addTemplate(m, t)
t = mm.newTemplate(_("Reverse"))
t['qfmt'] = "{{" + _("Back") + "}}"
t['afmt'] = "{{" + _("Front") + "}}"
t['actv'] = False
m.addTemplate(t)
mm.addTemplate(m, t)
mm.save(m)
return m
models.append((_("Basic"), BasicModel))
models.append((_("Basic"), addBasicModel))
# Cloze
##########################################################################
def ClozeModel(deck):
m = Model(deck)
m.name = _("Cloze")
fm = m.newField()
fm['name'] = _("Text")
def addClozeModel(deck):
mm = deck.models
m = mm.new(_("Cloze"))
fm = mm.newField(_("Text"))
fm['req'] = True
fm['uniq'] = True
m.addField(fm)
fm = m.newField()
fm['name'] = _("Notes")
m.addField(fm)
mm.addField(m, fm)
fm = mm.newField(_("Notes"))
mm.addField(m, fm)
for i in range(8):
n = i+1
t = m.newTemplate()
t['name'] = _("Cloze") + " %d" % n
t = mm.newTemplate(_("Cloze") + " %d" % n)
t['qfmt'] = ("{{#cloze:%d:Text}}<br>{{cloze:%d:%s}}<br>"+
"{{/cloze:%d:Text}}") % (n, n, _("Text"), n)
t['afmt'] = ("{{cloze:%d:" + _("Text") + "}}") % n
t['afmt'] += "<br>{{" + _("Notes") + "}}"
m.addTemplate(t)
mm.addTemplate(m, t)
mm.save(m)
return m
models.append((_("Cloze"), ClozeModel))
models.append((_("Cloze"), addClozeModel))

View file

@ -9,7 +9,7 @@ from anki.lang import _
from anki.utils import intTime
from anki.db import DB
from anki.deck import _Deck
from anki.stdmodels import BasicModel, ClozeModel
from anki.stdmodels import addBasicModel, addClozeModel
from anki.errors import AnkiError
from anki.hooks import runHook
@ -34,8 +34,8 @@ def Deck(path, queue=True, lock=True):
_upgradeDeck(deck, ver)
elif create:
# add in reverse order so basic is default
deck.addModel(ClozeModel(deck))
deck.addModel(BasicModel(deck))
addClozeModel(deck)
addBasicModel(deck)
deck.save()
if lock:
deck.lock()
@ -67,9 +67,9 @@ create table if not exists deck (
lastSync integer not null,
qconf text not null,
conf text not null,
models text not null,
groups text not null,
gconf text not null,
data text not null
gconf text not null
);
create table if not exists cards (
@ -108,16 +108,6 @@ create table if not exists fsums (
csum integer not null
);
create table if not exists models (
id integer primary key,
mod integer not null,
name text not null,
flds text not null,
tmpls text not null,
conf text not null,
css text not null
);
create table if not exists graves (
time integer not null,
oid integer not null,
@ -148,13 +138,14 @@ values(1,0,0,0,%(v)s,0,'',0,'','','','','');
import anki.groups
if setDeckConf:
db.execute("""
update deck set qconf = ?, conf = ?, groups = ?, gconf = ?, data = ?""",
update deck set qconf = ?, conf = ?, models = ?, groups = ?, gconf = ?""",
simplejson.dumps(anki.deck.defaultQconf),
simplejson.dumps(anki.deck.defaultConf),
"{}",
simplejson.dumps({'1': {'name': _("Default"), 'conf': 1,
'mod': intTime()}}),
simplejson.dumps({'1': anki.groups.defaultConf}),
"{}")
simplejson.dumps({'1': anki.groups.defaultConf}))
def _updateIndices(db):
"Add indices to the DB."
@ -494,7 +485,7 @@ order by modelId, ordinal"""):
def _fixupModels(deck):
# rewrite model/template/field ids
models = deck.models()
models = deck.models.all()
deck.db.execute("delete from models")
times = {}
for c, m in enumerate(models.values()):

View file

@ -171,7 +171,7 @@ def entsToTxt(html):
##############################################################################
def hexifyID(id):
return "%x" % id
return "%x" % int(id)
def dehexifyID(id):
return int(id, 16)

View file

@ -12,7 +12,7 @@ def test_genCards():
f['Front'] = u'1'
f['Back'] = u'2'
deck.addFact(f)
cards = deck.genCards(f, f.model().templates)
cards = deck.genCards(f, f.model()['tmpls'])
assert len(cards) == 1
assert cards[0].ord == 1
assert deck.cardCount() == 2
@ -23,7 +23,7 @@ def test_genCards():
f['Front'] = u'1'
f['Back'] = u'2'
deck.addFact(f)
cards = deck.genCards(f, f.model().templates)
cards = deck.genCards(f, f.model()['tmpls'])
assert deck.cardCount() == 4
c = deck.db.list("select due from cards where fid = ?", f.id)
assert c[0] == c[1]

View file

@ -2,7 +2,7 @@
import os, re, datetime
from tests.shared import assertException, getEmptyDeck, testDir
from anki.stdmodels import BasicModel
from anki.stdmodels import addBasicModel
from anki import Deck
@ -53,8 +53,8 @@ def test_factAddDelete():
f = deck.newFact()
f['Front'] = u"one"; f['Back'] = u"two"
m = f.model()
m.templates[1]['actv'] = True
m.flush()
m['tmpls'][1]['actv'] = True
deck.models.save(m)
n = deck.addFact(f)
assert n == 2
# check q/a generation
@ -65,7 +65,7 @@ def test_factAddDelete():
assert not p
# now let's make a duplicate and test uniqueness
f2 = deck.newFact()
f2.model().fields[1]['req'] = True
f2.model()['flds'][1]['req'] = True
f2['Front'] = u"one"; f2['Back'] = u""
p = f2.problems()
assert p[0] == "unique"
@ -102,15 +102,15 @@ def test_fieldChecksum():
"select csum from fsums") == int("4b0e5a4c", 16)
# turning off unique and modifying the fact should delete the sum
m = f.model()
m.fields[0]['uniq'] = False
m.flush()
m['flds'][0]['uniq'] = False
deck.models.save(m)
f.flush()
assert deck.db.scalar(
"select count() from fsums") == 0
# and turning on both should ensure two checksums generated
m.fields[0]['uniq'] = True
m.fields[1]['uniq'] = True
m.flush()
m['flds'][0]['uniq'] = True
m['flds'][1]['uniq'] = True
deck.models.save(m)
f.flush()
assert deck.db.scalar(
"select count() from fsums") == 2
@ -190,8 +190,8 @@ def test_addDelTags():
def test_timestamps():
deck = getEmptyDeck()
assert len(deck.models()) == 2
assert len(deck.models.models) == 2
for i in range(100):
deck.addModel(BasicModel(deck))
assert len(deck.models()) == 102
addBasicModel(deck)
assert len(deck.models.models) == 102

View file

@ -25,7 +25,7 @@ def test_findCards():
f = deck.newFact()
f['Front'] = u'template test'
f['Back'] = u'foo bar'
f.model().templates[1]['actv'] = True
f.model()['tmpls'][1]['actv'] = True
deck.addFact(f)
latestCardIds = [c.id for c in f.cards()]
# tag searches

View file

@ -2,7 +2,6 @@
import os
from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
from anki.utils import stripHTML, intTime
from anki.hooks import addHook

View file

@ -1,7 +1,6 @@
# coding: utf-8
from tests.shared import getEmptyDeck
from anki.models import Model
from anki.utils import stripHTML
def test_modelDelete():
@ -11,20 +10,20 @@ def test_modelDelete():
f['Back'] = u'2'
deck.addFact(f)
assert deck.cardCount() == 1
deck.delModel(deck.conf['currentModelId'])
deck.models.del_(deck.models.get(deck.conf['currentModelId']))
assert deck.cardCount() == 0
def test_modelCopy():
deck = getEmptyDeck()
m = deck.currentModel()
m2 = m.copy()
assert m2.name == "Basic copy"
assert m2.id != m.id
assert len(m2.fields) == 2
assert len(m.fields) == 2
assert len(m2.fields) == len(m.fields)
assert len(m.templates) == 2
assert len(m2.templates) == 2
m = deck.models.current()
m2 = deck.models.copy(m)
assert m2['name'] == "Basic copy"
assert m2['id'] != m['id']
assert len(m2['flds']) == 2
assert len(m['flds']) == 2
assert len(m2['flds']) == len(m['flds'])
assert len(m['tmpls']) == 2
assert len(m2['tmpls']) == 2
def test_fields():
d = getEmptyDeck()
@ -32,50 +31,50 @@ def test_fields():
f['Front'] = u'1'
f['Back'] = u'2'
d.addFact(f)
m = d.currentModel()
m = d.models.current()
# make sure renaming a field updates the templates
m.renameField(m.fields[0], "NewFront")
assert m.templates[0]['qfmt'] == "{{NewFront}}"
d.models.renameField(m, m['flds'][0], "NewFront")
assert m['tmpls'][0]['qfmt'] == "{{NewFront}}"
# add a field
f = m.newField()
f = d.models.newField(m)
f['name'] = "foo"
m.addField(f)
assert d.getFact(m.fids()[0]).fields == ["1", "2", ""]
d.models.addField(m, f)
assert d.getFact(d.models.fids(m)[0]).fields == ["1", "2", ""]
# rename it
m.renameField(f, "bar")
assert d.getFact(m.fids()[0])['bar'] == ''
d.models.renameField(m, f, "bar")
assert d.getFact(d.models.fids(m)[0])['bar'] == ''
# delete back
m.delField(m.fields[1])
assert d.getFact(m.fids()[0]).fields == ["1", ""]
d.models.delField(m, m['flds'][1])
assert d.getFact(d.models.fids(m)[0]).fields == ["1", ""]
# move 0 -> 1
m.moveField(m.fields[0], 1)
assert d.getFact(m.fids()[0]).fields == ["", "1"]
d.models.moveField(m, m['flds'][0], 1)
assert d.getFact(d.models.fids(m)[0]).fields == ["", "1"]
# move 1 -> 0
m.moveField(m.fields[1], 0)
assert d.getFact(m.fids()[0]).fields == ["1", ""]
d.models.moveField(m, m['flds'][1], 0)
assert d.getFact(d.models.fids(m)[0]).fields == ["1", ""]
# add another and put in middle
f = m.newField()
f = d.models.newField(m)
f['name'] = "baz"
m.addField(f)
f = d.getFact(m.fids()[0])
d.models.addField(m, f)
f = d.getFact(d.models.fids(m)[0])
f['baz'] = "2"
f.flush()
assert d.getFact(m.fids()[0]).fields == ["1", "", "2"]
assert d.getFact(d.models.fids(m)[0]).fields == ["1", "", "2"]
# move 2 -> 1
m.moveField(m.fields[2], 1)
assert d.getFact(m.fids()[0]).fields == ["1", "2", ""]
d.models.moveField(m, m['flds'][2], 1)
assert d.getFact(d.models.fids(m)[0]).fields == ["1", "2", ""]
# move 0 -> 2
m.moveField(m.fields[0], 2)
assert d.getFact(m.fids()[0]).fields == ["2", "", "1"]
d.models.moveField(m, m['flds'][0], 2)
assert d.getFact(d.models.fids(m)[0]).fields == ["2", "", "1"]
# move 0 -> 1
m.moveField(m.fields[0], 1)
assert d.getFact(m.fids()[0]).fields == ["", "2", "1"]
d.models.moveField(m, m['flds'][0], 1)
assert d.getFact(d.models.fids(m)[0]).fields == ["", "2", "1"]
def test_templates():
d = getEmptyDeck()
m = d.currentModel()
m.templates[1]['actv'] = True
m.flush()
m = d.models.current()
m['tmpls'][1]['actv'] = True
d.models.save(m)
f = d.newFact()
f['Front'] = u'1'
f['Back'] = u'2'
@ -86,12 +85,12 @@ def test_templates():
assert c.ord == 0
assert c2.ord == 1
# switch templates
m.moveTemplate(c.template(), 1)
d.models.moveTemplate(m, c.template(), 1)
c.load(); c2.load()
assert c.ord == 1
assert c2.ord == 0
# removing a template should delete its cards
m.delTemplate(m.templates[0])
d.models.delTemplate(m, m['tmpls'][0])
assert d.cardCount() == 1
# and should have updated the other cards' ordinals
c = f.cards()[0]
@ -100,9 +99,9 @@ def test_templates():
def test_text():
d = getEmptyDeck()
m = d.currentModel()
m.templates[0]['qfmt'] = "{{text:Front}}"
m.flush()
m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
d.models.save(m)
f = d.newFact()
f['Front'] = u'hello<b>world'
d.addFact(f)
@ -110,9 +109,9 @@ def test_text():
def test_cloze():
d = getEmptyDeck()
d.conf['currentModelId'] = d.modelId("Cloze")
d.conf['currentModelId'] = d.models.byName("Cloze")['id']
f = d.newFact()
assert f.model().name == "Cloze"
assert f.model()['name'] == "Cloze"
# a cloze model with no clozes is empty
f['Text'] = u'nothing'
assert d.addFact(f) == 0
@ -124,7 +123,7 @@ def test_cloze():
assert "<span class=cloze>world</span>" in f.cards()[0].a()
assert "hello <span class=cloze>world</span>" not in f.cards()[0].a()
# check context works too
f.model().conf['clozectx'] = True
f.model()['clozectx'] = True
assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
# and with a comment
f = d.newFact()
@ -143,16 +142,16 @@ def test_cloze():
assert "world <span class=cloze>bar</span>" in c2.a()
# if there are multiple answers for a single cloze, they are given in a
# list
f.model().conf['clozectx'] = False
f.model()['clozectx'] = False
f = d.newFact()
f['Text'] = "a {{c1::b}} {{c1::c}}"
assert d.addFact(f) == 1
assert "<span class=cloze>b</span>, <span class=cloze>c</span>" in (
f.cards()[0].a())
# clozes should be supported in sections too
m = d.currentModel()
m.templates[0]['qfmt'] = "{{#cloze:1:Text}}{{Notes}}{{/cloze:1:Text}}"
m.flush()
m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{#cloze:1:Text}}{{Notes}}{{/cloze:1:Text}}"
d.models.save(m)
f = d.newFact()
f['Text'] = "hello"
f['Notes'] = "world"
@ -162,18 +161,18 @@ def test_cloze():
def test_modelChange():
deck = getEmptyDeck()
basic = deck.getModel(deck.modelId("Basic"))
cloze = deck.getModel(deck.modelId("Cloze"))
basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze")
# enable second template and add a fact
basic.templates[1]['actv'] = True
basic.flush()
basic['tmpls'][1]['actv'] = True
deck.models.save(basic)
f = deck.newFact()
f['Front'] = u'f'
f['Back'] = u'b'
deck.addFact(f)
# switch fields
map = {0: 1, 1: 0}
basic.changeModel([f.id], basic, map, None)
deck.models.change(basic, [f.id], basic, map, None)
f.load()
assert f['Front'] == 'b'
assert f['Back'] == 'f'
@ -184,7 +183,7 @@ def test_modelChange():
assert stripHTML(c1.q()) == "f"
assert c0.ord == 0
assert c1.ord == 1
basic.changeModel([f.id], basic, None, map)
deck.models.change(basic, [f.id], basic, None, map)
f.load(); c0.load(); c1.load()
assert stripHTML(c0.q()) == "f"
assert stripHTML(c1.q()) == "b"
@ -194,7 +193,7 @@ def test_modelChange():
assert f.cards()[0].id == c1.id
# delete first card
map = {0: None, 1: 1}
basic.changeModel([f.id], basic, None, map)
deck.models.change(basic, [f.id], basic, None, map)
f.load()
c0.load()
try:
@ -206,7 +205,7 @@ def test_modelChange():
# an unmapped field becomes blank
assert f['Front'] == 'b'
assert f['Back'] == 'f'
basic.changeModel([f.id], basic, map, None)
deck.models.change(basic, [f.id], basic, map, None)
f.load()
assert f['Front'] == ''
assert f['Back'] == 'f'
@ -215,12 +214,21 @@ def test_modelChange():
f['Front'] = u'f2'
f['Back'] = u'b2'
deck.addFact(f)
assert basic.useCount() == 2
assert cloze.useCount() == 0
assert deck.models.useCount(basic) == 2
assert deck.models.useCount(cloze) == 0
map = {0: 0, 1: 1}
basic.changeModel([f.id], cloze, map, map)
deck.models.change(basic, [f.id], cloze, map, map)
f.load()
assert f['Text'] == "f2"
assert f['Notes'] == "b2"
assert len(f.cards()) == 2
assert "b2" in f.cards()[0].a()
def test_css():
deck = getEmptyDeck()
basic = deck.models.byName("Basic")
assert "arial" in basic['css']
assert "helvetica" not in basic['css']
basic['flds'][0]['font'] = "helvetica"
deck.models.save(basic)
assert "helvetica" in basic['css']

View file

@ -2,7 +2,6 @@
import time, copy
from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
from anki.utils import stripHTML, intTime
from anki.hooks import addHook
@ -32,9 +31,9 @@ def test_new():
assert c.due >= t
# the default order should ensure siblings are not seen together, and
# should show all cards
m = d.currentModel()
m.templates[1]['actv'] = True
m.flush()
m = d.models.current()
m['tmpls'][1]['actv'] = True
d.models.save(m)
f = d.newFact()
f['Front'] = u"2"; f['Back'] = u"2"
d.addFact(f)
@ -50,15 +49,15 @@ def test_new():
def test_newOrder():
d = getEmptyDeck()
m = d.currentModel()
m = d.models.current()
for i in range(50):
t = m.newTemplate()
t = d.models.newTemplate(m)
t['name'] = str(i)
t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}"
t['actv'] = i > 25
m.addTemplate(t)
m.flush()
d.models.addTemplate(m, t)
d.models.save(m)
f = d.newFact()
f['Front'] = u'1'
f['Back'] = u'2'
@ -495,19 +494,19 @@ def test_cramLimits():
def test_adjIvl():
d = getEmptyDeck()
# add two more templates and set second active
m = d.currentModel()
m.templates[1]['actv'] = True
t = m.newTemplate()
m = d.models.current()
m['tmpls'][1]['actv'] = True
t = d.models.newTemplate(m)
t['name'] = "f2"
t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}"
m.addTemplate(t)
t = m.newTemplate()
d.models.addTemplate(m, t)
t = d.models.newTemplate(m)
t['name'] = "f3"
t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}"
m.addTemplate(t)
m.flush()
d.models.addTemplate(m, t)
d.models.save(m)
# create a new fact; it should have 4 cards
f = d.newFact()
f['Front'] = "1"; f['Back'] = "1"
@ -560,14 +559,14 @@ def test_adjIvl():
def test_ordcycle():
d = getEmptyDeck()
# add two more templates and set second active
m = d.currentModel()
m.templates[1]['actv'] = True
t = m.newTemplate()
m = d.models.current()
m['tmpls'][1]['actv'] = True
t = d.models.newTemplate(m)
t['name'] = "f2"
t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}"
m.addTemplate(t)
m.flush()
d.models.addTemplate(m, t)
d.models.save(m)
# create a new fact; it should have 4 cards
f = d.newFact()
f['Front'] = "1"; f['Back'] = "1"

View file

@ -2,7 +2,6 @@
import time, copy, os
from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
from anki.utils import stripHTML, intTime
from anki.hooks import addHook

View file

@ -6,7 +6,6 @@ from tests.shared import assertException
from anki.errors import *
from anki import Deck
from anki.utils import intTime
from anki.stdmodels import BasicModel
from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy
from anki.sync import copyLocalMedia
from anki.facts import Fact

View file

@ -2,8 +2,6 @@
import time
from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
def test_op():
d = getEmptyDeck()
# should have no undo by default