From 1d29c7bcc0142875c1efe8bb212a46b1149aaf83 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 7 Mar 2012 05:45:52 +0900 Subject: [PATCH] foreign card importing; mnemosyne 2.0 importer --- anki/importing/__init__.py | 1 + anki/importing/mnemo.py | 134 +++++++++++++++++++++++++++++++++++++ anki/importing/noteimp.py | 27 ++++++++ tests/support/mnemo.db | Bin 0 -> 114688 bytes tests/test_importing.py | 11 ++- 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 anki/importing/mnemo.py create mode 100644 tests/support/mnemo.db diff --git a/anki/importing/__init__.py b/anki/importing/__init__.py index d6dd7ed13..997694cf6 100644 --- a/anki/importing/__init__.py +++ b/anki/importing/__init__.py @@ -7,6 +7,7 @@ from anki.importing.apkg import AnkiPackageImporter from anki.importing.anki2 import Anki2Importer from anki.importing.anki1 import Anki1Importer from anki.importing.supermemo_xml import SupermemoXmlImporter +from anki.importing.mnemo import MnemosyneImporter from anki.lang import _ Importers = ( diff --git a/anki/importing/mnemo.py b/anki/importing/mnemo.py new file mode 100644 index 000000000..8b8b79103 --- /dev/null +++ b/anki/importing/mnemo.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright: Damien Elmes +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import time, re +from anki.db import DB +from anki.importing.base import Importer +from anki.importing.noteimp import NoteImporter, ForeignNote, ForeignCard +from anki.utils import checksum, base91 +from anki.stdmodels import addBasicModel + +class MnemosyneImporter(NoteImporter): + + def run(self): + db = DB(self.file) + ver = db.scalar( + "select value from global_variables where key='version'") + assert ver.startswith('Mnemosyne SQL 1') + # gather facts into temp objects + curid = None + notes = {} + note = None + for _id, id, k, v in db.execute(""" +select _id, id, key, value from facts f, data_for_fact d where +f._id=d._fact_id"""): + if id != curid: + if note: + notes[note['_id']] = note + note = {'_id': _id} + curid = id + note[k] = v + if note: + notes[note['_id']] = note + # gather cards + front = [] + frontback = [] + vocabulary = [] + for row in db.execute(""" +select _fact_id, fact_view_id, tags, next_rep, last_rep, easiness, +acq_reps+ret_reps, lapses from cards"""): + # categorize note + note = notes[row[0]] + if row[1] == "1.1": + front.append(note) + elif row[1] == "2.1": + frontback.append(note) + elif row[1] == "3.1": + vocabulary.append(note) + # merge tags into note + tags = row[2].replace(", ", "\x1f").replace(" ", "_") + tags = tags.replace("\x1f", " ") + if "tags" not in note: + note['tags'] = [] + note['tags'] += self.col.tags.split(tags) + note['tags'] = self.col.tags.canonify(note['tags']) + # if it's a new card we can go with the defaults + if row[3] == -1: + continue + # add the card + c = ForeignCard() + c.factor = row[5] + c.reps = row[6] + c.lapses = row[7] + # ivl is inferred in mnemosyne + next, prev = row[3:5] + c.ivl = max(1, (next - prev)/86400) + # work out how long we've got left + rem = int((next - time.time())/86400) + c.due = max(0, self.col.sched.today+rem) + # get ord + m = re.match("\d+\.(\d+)", row[1]) + ord = int(m.group(1))-1 + if 'cards' not in note: + note['cards'] = {} + note['cards'][ord] = c + self._addFronts(front) + self._addFrontBacks(frontback) + self._addVocabulary(vocabulary) + + def fields(self): + return self._fields + + def _addFronts(self, notes, model=None, fields=("f", "b")): + data = [] + for orig in notes: + # create a foreign note object + n = ForeignNote() + n.fields = [] + for f in fields: + n.fields.append(orig.get(f, '')) + n.tags = orig['tags'] + n.cards = orig.get('cards', {}) + data.append(n) + # add a basic model + if not model: + model = addBasicModel(self.col) + model['name'] = "Mnemosyne-FrontOnly" + mm = self.col.models + mm.save(model) + mm.setCurrent(model) + self.model = model + self._fields = len(model['flds']) + self.initMapping() + # import + self.importNotes(data) + + def _addFrontBacks(self, notes): + m = addBasicModel(self.col) + m['name'] = "Mnemosyne-FrontBack" + mm = self.col.models + t = mm.newTemplate("Back") + t['qfmt'] = "{{Back}}" + t['afmt'] = t['qfmt'] + "\n\n
\n\n{{Front}}" + mm.addTemplate(m, t) + self._addFronts(notes, m) + + def _addVocabulary(self, notes): + mm = self.col.models + m = mm.new("Mnemosyne-Vocabulary") + for f in "Expression", "Pronunciation", "Meaning", "Notes": + fm = mm.newField(f) + mm.addField(m, fm) + t = mm.newTemplate("Recognition") + t['qfmt'] = "{{Expression}}" + t['afmt'] = t['qfmt'] + """\n\n
\n\n\ +{{Pronunciation}}
\n{{Meaning}}
\n{{Notes}}""" + mm.addTemplate(m, t) + t = mm.newTemplate("Production") + t['qfmt'] = "{{Meaning}}" + t['afmt'] = t['qfmt'] + """\n\n
\n\n\ +{{Expression}}
\n{{Pronunciation}}
\n{{Notes}}""" + mm.addTemplate(m, t) + mm.add(m) + self._addFronts(notes, m, fields=("f", "p_1", "m_1", "n")) diff --git a/anki/importing/noteimp.py b/anki/importing/noteimp.py index 603af1373..7bdef12ff 100644 --- a/anki/importing/noteimp.py +++ b/anki/importing/noteimp.py @@ -18,6 +18,15 @@ class ForeignNote(object): self.fields = [] self.tags = [] self.deck = None + self.cards = {} # map of ord -> card + +class ForeignCard(object): + def __init__(self): + self.due = 0 + self.ivl = 1 + self.factor = 2.5 + self.reps = 0 + self.lapses = 0 # Base class for CSV and similar text-based imports ###################################################################### @@ -84,6 +93,7 @@ class NoteImporter(Importer): updates = [] new = [] self._ids = [] + self._cards = [] for n in notes: fld0 = n.fields[fld0idx] csum = fieldChecksum(fld0) @@ -122,7 +132,10 @@ class NoteImporter(Importer): self.addNew(new) self.addUpdates(updates) self.col.updateFieldCache(self._ids) + # generate cards assert not self.col.genCards(self._ids) + # apply scheduling updates + self.updateCards() # make sure to update sflds, etc self.total = len(self._ids) @@ -133,6 +146,10 @@ class NoteImporter(Importer): if not self.processFields(n): print "no cards generated" return + # note id for card updates later + for ord, c in n.cards.items(): + self._cards.append((id, ord, c)) + self.col.tags.register(n.tags) return [id, guid64(), self.model['id'], self.didForNote(n), intTime(), self.col.usn(), self.col.tags.join(n.tags), n.fieldsStr, "", "", 0, ""] @@ -148,6 +165,7 @@ class NoteImporter(Importer): if not self.processFields(n): print "no cards generated" return + self.col.tags.register(n.tags) tags = self.col.tags.join(n.tags) return [intTime(), self.col.usn(), n.fieldsStr, tags, id, n.fieldsStr, tags] @@ -178,3 +196,12 @@ where id = ? and (flds != ? or tags != ?)""", rows) fields[sidx] = note.fields[c] note.fieldsStr = joinFields(fields) return self.col.models.availOrds(self.model, note.fieldsStr) + + def updateCards(self): + data = [] + for nid, ord, c in self._cards: + data.append((c.ivl, c.due, c.factor, c.reps, c.lapses, nid, ord)) + # we assume any updated cards are reviews + self.col.db.executemany(""" +update cards set type = 2, queue = 2, ivl = ?, due = ?, +factor = ?, reps = ?, lapses = ? where nid = ? and ord = ?""", data) diff --git a/tests/support/mnemo.db b/tests/support/mnemo.db new file mode 100644 index 0000000000000000000000000000000000000000..677ed73e5e342129840173f260e3725328c75620 GIT binary patch literal 114688 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 M!}gI~FWLG37Z?5n