diff --git a/anki/collection.py b/anki/collection.py index 7ca632f92..61710b6a8 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -642,6 +642,9 @@ select id from notes where mid not in """ + ids2str(self.models.ids())) self.remNotes(ids) # cards with invalid ordinal for m in self.models.all(): + # ignore clozes + if m['type'] != MODEL_STD: + continue ids = self.db.list(""" select id from cards where ord not in %s and nid in ( select id from notes where mid = ?)""" % diff --git a/anki/importing/anki2.py b/anki/importing/anki2.py index c114ba5bb..64b9acb56 100644 --- a/anki/importing/anki2.py +++ b/anki/importing/anki2.py @@ -4,11 +4,10 @@ import os from anki import Collection -from anki.utils import intTime, splitFields, joinFields, checksum +from anki.utils import intTime, splitFields, joinFields, checksum, guid64 from anki.importing.base import Importer from anki.lang import _ from anki.lang import ngettext -from anki.hooks import runFilter # # Import a .anki2 file into the current collection. Used for migration from @@ -43,7 +42,6 @@ class Anki2Importer(Importer): def _import(self): self._decks = {} - self._prepareDeckPrefix() if self.deckPrefix: id = self.dst.decks.id(self.deckPrefix) self.dst.decks.select(id) @@ -56,24 +54,6 @@ class Anki2Importer(Importer): self.dst.db.execute("vacuum") self.dst.db.execute("analyze") - def _prepareDeckPrefix(self): - if self.deckPrefix: - return runFilter("prepareImportPrefix", self.deckPrefix) - prefix = None - for deck in self.src.decks.all(): - if str(deck['id']) == "1": - # we can ignore the default deck if it's empty - if not self.src.db.scalar( - "select 1 from cards where did = ? limit 1", deck['id']): - continue - head = deck['name'].split("::")[0] - if not prefix: - prefix = head - else: - if prefix != head: - return - self.deckPrefix = runFilter("prepareImportPrefix", prefix) - # Notes ###################################################################### # - should note new for wizard @@ -86,6 +66,9 @@ class Anki2Importer(Importer): "select id, guid, mod, mid from notes"): self._notes[guid] = (id, mod, mid) existing[id] = True + # we may need to rewrite the guid if the model schemas don't match, + # so we need to keep track of the changes for the card import stage + self._changedGuids = {} # iterate over source collection add = [] dirty = [] @@ -96,8 +79,22 @@ class Anki2Importer(Importer): # turn the db result into a mutable list note = list(note) guid, mid = note[1:3] - # missing from local col? - if guid not in self._notes: + canUseExisting = False + alreadyHaveGuid = False + # do we have the same guid? + if guid in self._notes: + alreadyHaveGuid = True + # and do they share the same model id? + if self._notes[guid][2] == mid: + # and do they share the same schema? + srcM = self.src.models.get(mid) + dstM = self.dst.models.get(self._notes[guid][2]) + if (self.src.models.scmhash(srcM) == + self.src.models.scmhash(dstM)): + # then it's safe to treat as an exact duplicate + canUseExisting = True + # if we can't reuse an existing one, we'll need to add new + if not canUseExisting: # get corresponding local model lmid = self._mid(mid) # ensure id is unique @@ -111,11 +108,16 @@ class Anki2Importer(Importer): note[6] = self._mungeMedia(mid, note[6]) add.append(note) dirty.append(note[0]) + # if it was originally the same as a note in this deck but the + # models have diverged, we need to change the guid + if alreadyHaveGuid: + guid = guid64() + self._changedGuids[note[1]] = guid + note[1] = guid # note we have the added note self._notes[guid] = (note[0], note[3], note[2]) else: dupes += 1 - pass ## update existing note - not yet tested; for post 2.0 # newer = note[3] > mod # if self.allowUpdate and self._mid(mid) == mid and newer: @@ -245,6 +247,8 @@ class Anki2Importer(Importer): "select f.guid, f.mid, c.* from cards c, notes f " "where c.nid = f.id"): guid = card[0] + if guid in self._changedGuids: + guid = self._changedGuids[guid] # does the card's note exist in dst col? if guid not in self._notes: continue diff --git a/anki/latex.py b/anki/latex.py index 90cfd5e78..58a017dff 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -64,7 +64,7 @@ def _latexFromHtml(col, latex): # entitydefs defines nbsp as \xa0 instead of a standard space, so we # replace it first latex = latex.replace(" ", " ") - latex = re.sub("|", "\n", latex) + latex = re.sub("|
", "\n", latex) # replace
etc with spaces latex = re.sub("<.+?>", " ", latex) latex = stripHTML(latex) diff --git a/anki/models.py b/anki/models.py index e6ca38c67..e4044875d 100644 --- a/anki/models.py +++ b/anki/models.py @@ -452,6 +452,8 @@ select id from notes where mid = ?)""" % " ".join(map), s = "" for f in m['flds']: s += f['name'] + for t in m['tmpls']: + s += t['name'] return fieldChecksum(s) # Required field/text cache diff --git a/anki/sched.py b/anki/sched.py index 6054b6f9b..e3a8df4d0 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -119,7 +119,7 @@ order by due""" % self._deckLimit(), if card.odid and card.queue == 2: return 4 conf = self._lapseConf(card) - if len(conf['delays']) > 1: + if card.type == 0 or len(conf['delays']) > 1: return 3 return 2 elif card.queue == 2: diff --git a/anki/stats.py b/anki/stats.py index e8cac5061..ada18bbd8 100644 --- a/anki/stats.py +++ b/anki/stats.py @@ -139,7 +139,7 @@ body {background-image: url(data:image/png;base64,%s); } lim = " and " + lim cards, thetime, failed, lrn, rev, relrn, filt = self.col.db.first(""" select count(), sum(time)/1000, -sum(case when ease = 0 then 1 else 0 end), /* failed */ +sum(case when ease = 1 then 1 else 0 end), /* failed */ sum(case when type = 0 then 1 else 0 end), /* learning */ sum(case when type = 1 then 1 else 0 end), /* review */ sum(case when type = 2 then 1 else 0 end), /* relearn */ @@ -154,7 +154,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000) filt = filt or 0 # studied def bold(s): - return ""+str(s)+"" + return ""+unicode(s)+"" msgp1 = ngettext("%d card", "%d cards", cards) % cards b += _("Studied %(a)s in %(b)s today.") % dict( a=bold(msgp1), b=bold(fmtTimeSpan(thetime, unit=1))) @@ -828,7 +828,7 @@ $(function () { def _avgDay(self, tot, num, unit): vals = [] try: - vals.append(_("%(a)d %(b)s/day") % dict(a=tot/float(num), b=unit)) + vals.append(_("%(a)0.1f %(b)s/day") % dict(a=tot/float(num), b=unit)) return ", ".join(vals) except ZeroDivisionError: return "" diff --git a/anki/upgrade.py b/anki/upgrade.py index 10c4b9e07..16407d1a0 100644 --- a/anki/upgrade.py +++ b/anki/upgrade.py @@ -341,11 +341,8 @@ insert or replace into col select id, cast(created as int), :t, mods = {} for row in db.all( "select id, name from models"): - while 1: - t = intTime(1000) - if t not in times: - times[t] = True - break + # use only first 31 bits + t = abs(row[0]) >> 32 m = anki.models.defaultModel.copy() m['id'] = t m['name'] = row[1] diff --git a/tests/support/diffmodels1.anki b/tests/support/diffmodels1.anki new file mode 100644 index 000000000..aa2103b05 Binary files /dev/null and b/tests/support/diffmodels1.anki differ diff --git a/tests/support/diffmodels2-1.apkg b/tests/support/diffmodels2-1.apkg new file mode 100644 index 000000000..e3c85af81 Binary files /dev/null and b/tests/support/diffmodels2-1.apkg differ diff --git a/tests/support/diffmodels2-2.apkg b/tests/support/diffmodels2-2.apkg new file mode 100644 index 000000000..06534f446 Binary files /dev/null and b/tests/support/diffmodels2-2.apkg differ diff --git a/tests/support/diffmodels2.anki b/tests/support/diffmodels2.anki new file mode 100644 index 000000000..0f921c90a Binary files /dev/null and b/tests/support/diffmodels2.anki differ diff --git a/tests/test_importing.py b/tests/test_importing.py index a40e68bb2..8f5134b13 100644 --- a/tests/test_importing.py +++ b/tests/test_importing.py @@ -138,6 +138,48 @@ def test_anki1(): imp.run() check() +def test_anki1_diffmodels(): + # create a new empty deck + dst = getEmptyDeck() + # import the 1 card version of the model + tmp = getUpgradeDeckPath("diffmodels1.anki") + imp = Anki1Importer(dst, tmp) + imp.run() + before = dst.noteCount() + # repeating the process should do nothing + imp = Anki1Importer(dst, tmp) + imp.run() + assert before == dst.noteCount() + # then the 2 card version + tmp = getUpgradeDeckPath("diffmodels2.anki") + imp = Anki1Importer(dst, tmp) + imp.run() + after = dst.noteCount() + # as the model schemas differ, should have been imported as new model + assert after == before + 1 + +def test_anki2_diffmodels(): + # create a new empty deck + dst = getEmptyDeck() + # import the 1 card version of the model + tmp = getUpgradeDeckPath("diffmodels2-1.apkg") + imp = AnkiPackageImporter(dst, tmp) + imp.run() + before = dst.noteCount() + # repeating the process should do nothing + imp = AnkiPackageImporter(dst, tmp) + imp.run() + assert before == dst.noteCount() + # then the 2 card version + tmp = getUpgradeDeckPath("diffmodels2-2.apkg") + imp = AnkiPackageImporter(dst, tmp) + imp.run() + after = dst.noteCount() + # as the model schemas differ, should have been imported as new model + assert after == before + 1 + # and the new model should have both cards + assert dst.cardCount() == 3 + def test_csv(): deck = getEmptyDeck() file = unicode(os.path.join(testDir, "support/text-2fields.txt"))