mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
345 lines
10 KiB
Python
345 lines
10 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 simplejson
|
|
from anki.utils import intTime, hexifyID, joinFields, splitFields
|
|
from anki.lang import _
|
|
|
|
# Models
|
|
##########################################################################
|
|
# gid may point to non-existent group
|
|
|
|
defaultConf = {
|
|
'sortf': 0,
|
|
'gid': 1,
|
|
}
|
|
|
|
defaultField = {
|
|
'name': "",
|
|
'ord': None,
|
|
'rtl': False,
|
|
'req': False,
|
|
'uniq': False,
|
|
'font': "Arial",
|
|
'qsize': 20,
|
|
'esize': 20,
|
|
'qcol': "#fff",
|
|
'pre': True,
|
|
}
|
|
|
|
defaultTemplate = {
|
|
'name': "",
|
|
'ord': None,
|
|
'actv': True,
|
|
'qfmt': "",
|
|
'afmt': "",
|
|
'hideQ': False,
|
|
'align': 0,
|
|
'bg': "#000",
|
|
'emptyAns': True,
|
|
'typeAns': None,
|
|
'gid': None
|
|
}
|
|
|
|
class Model(object):
|
|
|
|
def __init__(self, deck, id=None):
|
|
self.deck = deck
|
|
if id:
|
|
self.id = id
|
|
self.load()
|
|
else:
|
|
self.id = None
|
|
self.name = u""
|
|
self.mod = intTime()
|
|
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 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
|
|
|
|
def fids(self):
|
|
return self.deck.db.list(
|
|
"select id from facts where mid = ?", self.id)
|
|
|
|
def useCount(self):
|
|
return self.deck.db.scalar(
|
|
"select count() from facts where mid = ?", self.id)
|
|
|
|
# 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
|
|
|
|
# CSS generation
|
|
##################################################
|
|
|
|
def genCSS(self):
|
|
if not self.id:
|
|
return ""
|
|
# fields
|
|
css = "".join([self._fieldCSS(
|
|
".fm%s-%s" % (hexifyID(self.id), hexifyID(f['ord'])),
|
|
(f['font'], f['qsize'], f['qcol'], f['rtl'], f['pre']))
|
|
for f in self.fields])
|
|
# templates
|
|
css += "".join([".cm%s-%s {text-align:%s;background:%s}\n" % (
|
|
hexifyID(self.id), hexifyID(t['ord']),
|
|
("center", "left", "right")[t['align']], t['bg'])
|
|
for t in self.templates])
|
|
return css
|
|
|
|
def _rewriteFont(self, font):
|
|
"Convert a platform font to a multiplatform list."
|
|
font = font.lower()
|
|
for family in self.deck.conf['fontFamilies']:
|
|
for font2 in family:
|
|
if font == font2.lower():
|
|
return ",".join(family)
|
|
return font
|
|
|
|
def _fieldCSS(self, prefix, row):
|
|
(fam, siz, col, rtl, pre) = row
|
|
t = 'font-family:"%s";' % self._rewriteFont(fam)
|
|
t += 'font-size:%dpx;' % siz
|
|
t += 'color:%s;' % col
|
|
if rtl:
|
|
t += "direction:rtl;unicode-bidi:embed;"
|
|
if pre:
|
|
t += "white-space:pre-wrap;"
|
|
t = "%s {%s}\n" % (prefix, t)
|
|
return t
|
|
|
|
# Fields
|
|
##################################################
|
|
|
|
def fieldMap(self):
|
|
"Mapping of field name -> (ord, field)."
|
|
return dict([(f['name'], (f['ord'], f)) for f in self.fields])
|
|
|
|
def sortIdx(self):
|
|
return self.conf['sortf']
|
|
|
|
def setSortIdx(self, idx):
|
|
assert idx >= 0 and idx < len(self.fields)
|
|
self.deck.modSchema()
|
|
self.conf['sortf'] = idx
|
|
self.deck.updateFieldCache(self.fids(), csum=False)
|
|
self.flush()
|
|
|
|
def newField(self):
|
|
return defaultField.copy()
|
|
|
|
def addField(self, field):
|
|
self.fields.append(field)
|
|
self._updateFieldOrds()
|
|
self.flush()
|
|
def add(fields):
|
|
fields.append("")
|
|
return fields
|
|
self._transformFields(add)
|
|
|
|
def delField(self, field):
|
|
idx = self.fields.index(field)
|
|
self.fields.remove(field)
|
|
self._updateFieldOrds()
|
|
def delete(fields):
|
|
del fields[idx]
|
|
return fields
|
|
self._transformFields(delete)
|
|
if idx == self.sortIdx():
|
|
# need to rebuild
|
|
self.deck.updateFieldCache(self.fids(), csum=False)
|
|
# flushes
|
|
self.renameField(field, None)
|
|
|
|
def moveField(self, field, idx):
|
|
oldidx = self.fields.index(field)
|
|
if oldidx == idx:
|
|
return
|
|
self.fields.remove(field)
|
|
self.fields.insert(idx, field)
|
|
self._updateFieldOrds()
|
|
self.flush()
|
|
def move(fields, oldidx=oldidx):
|
|
val = fields[oldidx]
|
|
del fields[oldidx]
|
|
fields.insert(idx, val)
|
|
return fields
|
|
self._transformFields(move)
|
|
|
|
def renameField(self, field, newName):
|
|
self.deck.modSchema()
|
|
for t in self.templates:
|
|
types = ("{{%s}}", "{{text:%s}}", "{{#%s}}",
|
|
"{{^%s}}", "{{/%s}}")
|
|
for type in types:
|
|
for fmt in ('qfmt', 'afmt'):
|
|
if newName:
|
|
repl = type%newName
|
|
else:
|
|
repl = ""
|
|
t[fmt] = t[fmt].replace(type%field['name'], repl)
|
|
field['name'] = newName
|
|
self.flush()
|
|
|
|
def _updateFieldOrds(self):
|
|
for c, f in enumerate(self.fields):
|
|
f['ord'] = c
|
|
|
|
def _transformFields(self, fn):
|
|
self.deck.modSchema()
|
|
r = []
|
|
for (id, flds) in self.deck.db.execute(
|
|
"select id, flds from facts where mid = ?", self.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 addTemplate(self, template):
|
|
self.deck.modSchema()
|
|
self.templates.append(template)
|
|
self._updateTemplOrds()
|
|
self.flush()
|
|
|
|
def delTemplate(self, template):
|
|
self.deck.modSchema()
|
|
ord = self.templates.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)
|
|
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()
|
|
|
|
def _updateTemplOrds(self):
|
|
for c, t in enumerate(self.templates):
|
|
t['ord'] = c
|
|
|
|
def moveTemplate(self, template, idx):
|
|
oldidx = self.templates.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()
|
|
# generate change map
|
|
map = []
|
|
for t in self.templates:
|
|
map.append("when ord = %d then %d" % (oldidxs[id(t)], t['ord']))
|
|
# apply
|
|
self.flush()
|
|
self.deck.db.execute("""
|
|
update cards set ord = (case %s end) where fid in (
|
|
select id from facts where mid = ?)""" % " ".join(map), self.id)
|
|
|
|
# Model changing
|
|
##########################################################################
|
|
|
|
def changeModel(self, fids, newModel, fieldMap, cardMap):
|
|
raise Exception()
|
|
self.modSchema()
|
|
sfids = ids2str(fids)
|
|
# field remapping
|
|
if fieldMap:
|
|
seen = {}
|
|
for (old, new) in fieldMap.items():
|
|
seen[new] = 1
|
|
if new:
|
|
# can rename
|
|
self.db.execute("""
|
|
update fdata set
|
|
fmid = :new,
|
|
ord = :ord
|
|
where fmid = :old
|
|
and fid in %s""" % sfids, new=new.id, ord=new.ord, old=old.id)
|
|
else:
|
|
# no longer used
|
|
self.db.execute("""
|
|
delete from fdata where fid in %s
|
|
and fmid = :id""" % sfids, id=old.id)
|
|
# new
|
|
for field in newModel.fields:
|
|
if field not in seen:
|
|
d = [{'fid': f,
|
|
'fmid': field.id,
|
|
'ord': field.ord}
|
|
for f in fids]
|
|
self.db.executemany('''
|
|
insert into fdata
|
|
(fid, fmid, ord, value)
|
|
values
|
|
(:fid, :fmid, :ord, "")''', d)
|
|
# fact modtime
|
|
self.db.execute("""
|
|
update facts set
|
|
mod = :t,
|
|
mid = :id
|
|
where id in %s""" % sfids, t=time.time(), id=newModel.id)
|
|
# template remapping
|
|
toChange = []
|
|
for (old, new) in cardMap.items():
|
|
if not new:
|
|
# delete
|
|
self.db.execute("""
|
|
delete from cards
|
|
where tid = :cid and
|
|
fid in %s""" % sfids, cid=old.id)
|
|
elif old != new:
|
|
# gather ids so we can rename x->y and y->x
|
|
ids = self.db.list("""
|
|
select id from cards where
|
|
tid = :id and fid in %s""" % sfids, id=old.id)
|
|
toChange.append((new, ids))
|
|
for (new, ids) in toChange:
|
|
self.db.execute("""
|
|
update cards set
|
|
tid = :new,
|
|
ord = :ord
|
|
where id in %s""" % ids2str(ids), new=new.id, ord=new.ord)
|
|
cardIds = self.db.list(
|
|
"select id from cards where fid in %s" %
|
|
ids2str(fids))
|