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()