diff --git a/anki/models.py b/anki/models.py
index 01559a058..4c8633b53 100644
--- a/anki/models.py
+++ b/anki/models.py
@@ -3,7 +3,7 @@
# 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.utils import intTime, hexifyID, joinFields, splitFields, ids2str
from anki.lang import _
# Models
@@ -278,68 +278,45 @@ select id from facts where mid = ?)""" % " ".join(map), self.id)
# Model changing
##########################################################################
+ # - maps are ord->ord, and there should not be duplicate targets
+ # - newModel should be self if model is not changing
+ # - interface should ensure there's at least one remaining card
- 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))
+ def changeModel(self, fids, newModel, fmap, cmap):
+ self.deck.modSchema()
+ assert newModel.id == self.id or (fmap and cmap)
+ if fmap:
+ self._changeFacts(fids, newModel, fmap)
+ if cmap:
+ self._changeCards(fids, newModel, cmap)
+
+ def _changeFacts(self, fids, newModel, map):
+ d = []
+ nfields = len(newModel.fields)
+ for (fid, flds) in self.deck.db.execute(
+ "select id, flds from facts where id in "+ids2str(fids)):
+ newflds = {}
+ flds = splitFields(flds)
+ for old, new in map.items():
+ newflds[new] = flds[old]
+ flds = []
+ for c in range(nfields):
+ flds.append(newflds.get(c, ""))
+ flds = joinFields(flds)
+ 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)
+
+ def _changeCards(self, fids, newModel, map):
+ d = []
+ deleted = []
+ for (cid, ord) in self.deck.db.execute(
+ "select id, ord from cards where fid in "+ids2str(fids)):
+ if map[ord] is not None:
+ d.append(dict(cid=cid, new=map[ord]))
+ else:
+ deleted.append(cid)
+ self.deck.db.executemany(
+ "update cards set ord=:new where id=:cid", d)
+ self.deck.delCards(deleted)
diff --git a/tests/test_models.py b/tests/test_models.py
index d8b551eaf..8c8483677 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -148,69 +148,66 @@ def test_cloze():
assert d.addFact(f) == 1
def test_modelChange():
- print "model change"
- return
deck = getEmptyDeck()
- m2 = deck.currentModel()
- # taken from jp support plugin
- m1 = Model(deck)
- m1.name = "Japanese"
- # field 1
- fm = m1.newField()
- fm['name'] = "Expression"
- fm['req'] = True
- fm['uniq'] = True
- m1.addField(fm)
- # field2
- fm = m1.newField()
- fm['name'] = "Meaning"
- m1.addField(fm)
- # field3
- fm = m1.newField()
- fm['name'] = "Reading"
- m1.addField(fm)
- # template1
- t = Template(deck)
- t.name = "Recognition"
- t.qfmt = "{{Expression}}"
- t.afmt = "{{Reading}}
{{Meaning}}"
- m1.addTemplate(t)
- # template2
- t = Template(deck)
- t.name = "Recall"
- t.qfmt = "{{Meaning}}"
- t.afmt = "{{Expression}}
{{Reading}}"
- #t.active = False
- m1.addTemplate(t)
- deck.addModel(m1)
-
- # add some facts
+ basic = deck.getModel(1)
+ cloze = deck.getModel(2)
+ # enable second template and add a fact
+ basic.templates[1]['actv'] = True
+ basic.flush()
f = deck.newFact()
- f['Expression'] = u'e'
- f['Meaning'] = u'm'
- f['Reading'] = u'r'
+ f['Front'] = u'f'
+ f['Back'] = u'b'
deck.addFact(f)
- f2 = deck.newFact()
- f2['Expression'] = u'e2'
- f2['Meaning'] = u'm2'
- f2['Reading'] = u'r2'
- deck.addFact(f2)
-
- # convert to basic
- assert deck.modelUseCount(m1) == 2
- assert deck.modelUseCount(m2) == 0
- assert deck.cardCount() == 4
- assert deck.factCount() == 2
- fmap = {m1.fields[0]: m2.fields[0],
- m1.fields[1]: None,
- m1.fields[2]: m2.fields[1]}
- cmap = {m1.templates[0]: m2.templates[0],
- m1.templates[1]: None}
- deck.changeModel([f.id], m2, fmap, cmap)
- assert deck.modelUseCount(m1) == 1
- assert deck.modelUseCount(m2) == 1
- assert deck.cardCount() == 3
- assert deck.factCount() == 2
- c = deck.getCard(deck.db.scalar("select id from cards where fid = ?", f.id))
- assert stripHTML(c.q()) == u"e"
- assert stripHTML(c.a()) == u"r"
+ # switch fields
+ map = {0: 1, 1: 0}
+ basic.changeModel([f.id], basic, map, None)
+ f.load()
+ assert f['Front'] == 'b'
+ assert f['Back'] == 'f'
+ # switch cards
+ c0 = f.cards()[0]
+ c1 = f.cards()[1]
+ assert stripHTML(c0.q()) == "b"
+ assert stripHTML(c1.q()) == "f"
+ assert c0.ord == 0
+ assert c1.ord == 1
+ basic.changeModel([f.id], basic, None, map)
+ f.load(); c0.load(); c1.load()
+ assert stripHTML(c0.q()) == "f"
+ assert stripHTML(c1.q()) == "b"
+ assert c0.ord == 1
+ assert c1.ord == 0
+ # .cards() returns cards in order
+ assert f.cards()[0].id == c1.id
+ # delete first card
+ map = {0: None, 1: 1}
+ basic.changeModel([f.id], basic, None, map)
+ f.load()
+ c0.load()
+ try:
+ c1.load()
+ assert 0
+ except TypeError:
+ pass
+ assert len(f.cards()) == 1
+ # an unmapped field becomes blank
+ assert f['Front'] == 'b'
+ assert f['Back'] == 'f'
+ basic.changeModel([f.id], basic, map, None)
+ f.load()
+ assert f['Front'] == ''
+ assert f['Back'] == 'f'
+ # another fact to try model conversion
+ f = deck.newFact()
+ f['Front'] = u'f2'
+ f['Back'] = u'b2'
+ deck.addFact(f)
+ assert basic.useCount() == 2
+ assert cloze.useCount() == 0
+ map = {0: 0, 1: 1}
+ basic.changeModel([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()