diff --git a/anki/importing/__init__.py b/anki/importing/__init__.py
index b178a8d5a..83ce7495f 100644
--- a/anki/importing/__init__.py
+++ b/anki/importing/__init__.py
@@ -262,6 +262,7 @@ from anki.importing.anki10 import Anki10Importer
from anki.importing.mnemosyne10 import Mnemosyne10Importer
from anki.importing.wcu import WCUImporter
from anki.importing.supermemo_xml import SupermemoXmlImporter
+from anki.importing.dingsbums import DingsBumsImporter
Importers = (
(_("Text separated by tabs or semicolons (*)"), TextImporter),
@@ -269,4 +270,5 @@ Importers = (
(_("Mnemosyne Deck (*.mem)"), Mnemosyne10Importer),
(_("CueCard Deck (*.wcu)"), WCUImporter),
(_("Supermemo XML export (*.xml)"), SupermemoXmlImporter),
+ (_("DingsBums?! Deck (*.xml)"), DingsBumsImporter),
)
diff --git a/anki/importing/dingsbums.py b/anki/importing/dingsbums.py
new file mode 100644
index 000000000..7bb19619a
--- /dev/null
+++ b/anki/importing/dingsbums.py
@@ -0,0 +1,224 @@
+# -*- coding: utf-8 -*-
+# Copyright: rick@vanosten.net
+# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
+
+"""\
+Importing DingsBums?! decks (see dingsbums.vanosten.net)
+========================================================
+
+GENERAL:
+* DingsBums?! files are xml with relational content.
+* DingsBums?!'s data format is more relational than Anki's. Therefore some of the relations are denormalized.
+
+* A stack in DingsBums?! is a deck in Anki
+* An entry type in DingsBums?! is a model in Anki
+* An entry type attribute in DingsBums?! is a field in Anki
+* An entry type attribute item in DingsBums?! does not exist in Anki. It is just the contents of a field denormalized.
+* There is not concept of units and categories in Anki.
+* An entry in DingsBums?! is basically a fact in Anki
+* There are no cards in DingsBums?!
+* There is a special plugin in Anki for Pinyin. Therefore syllable settings from DingsBums?! are ignored.
+* The locale settings in DingsBums?! have never been active and are therefore ignored.
+* All statistics will get lost - i.e. no historic informaiton about progress will be migrated to Anki.
+* The DingsBums?! stack needs to end with *.xml in order to be recognizable in Anki import.
+* The learning levels from DingsBums?! are not taken into account because they do not really match spaced repetition.
+
+DESIGN OF MAPPING FROM DingsBums?! TO Anki
+*
+* The contents of units and categories are transferred as tags to Anki: unit/category label + "_" + unit/category name.
+* If unit/category name has space, then it is replaced by "_"
+* The fields "base", "target", explanation", example", "pronounciation" and "relation" are created as fields in Anki
+* The fields are only created and used in Anki, if they were visible in DingsBums?!, i.e. < 3:
+VISIBILITY_ALWAYS = 0;
+VISIBILITY_QUERY = 1;
+VISIBILITY_SOLUTION = 2;
+VISIBILITY_NEVER = 3;
+
+* The name of the fields in Anki is taken from the labels defined in the stack properties
+* The description field of Anki is not used/displayed. Therefore there is not much sense to transfer the contents of title, author, notes, copyright and license.
+* The visibility options in DingsBums?! are used as hints to make cards in Anki:
+ + Two card templates are made for each model and then applied to each fact when importing.
+ + "Forward": Base -> "Question", target -> "Answer"; if "always", then part of question; if "solution" or "part of query" then part of answer
+ + "Reverse": Target -> "Answer", base -> "Question"
+ + Unit and category are not shown, as they are tags and there is no possibility to distinguish between visibility settings in this case.
+
+CHANGES MADE TO LIBANKI:
+* Added libanki/anki/import/dingsbums.py
+* Added DingsBumsImporter to importers at end of file libanki/anki/import/__init__.py
+* Added libanki/tests/importing/dingsbums.xml
+* Added method test_dingsbums() to libanki/anki/tests/test_importing.py
+"""
+
+from anki.importing import Importer
+from anki import DeckStorage
+from anki.facts import Fact
+from anki.models import FieldModel
+from anki.models import CardModel
+from anki.models import Model
+from anki.lang import _
+
+from xml.sax import make_parser
+from xml.sax.handler import ContentHandler
+import sys
+
+class DingsBumsHandler(ContentHandler):
+
+ def __init__(self, deck):
+ self.eid = "0"
+ self.attributeItems = {}
+ self.unitCategories = {}
+ self.attributes = {}
+ self.currentContent = ""
+ self.labels = {}
+ self.labels["pro"] = u"Pronunciation" # the user cannot change this label and therefore not in xml-file
+ self.labels["rel"] = u"Relation"
+ self.visibility = {}
+ self.models = {}
+ self.typeAttributes = {} # mapping of entry type and attribute name (e.g. "ET8_A1", "ET8_A2", ...)
+ self.deck = deck
+ self.f = None # the current fact
+ self.countFacts = 0
+
+ def startElement(self, name, attrs):
+ """Implements SAX interface"""
+ if name in ["etai", "unit", "category"]:
+ self.eid = attrs["eid"]
+ elif "eta" == name:
+ self.attributes[attrs["eid"]] = attrs["n"]
+ elif "entrytype" == name:
+ self.createModel(attrs)
+ elif "e" == name:
+ self.createFact(attrs)
+
+ def endElement(self, name):
+ """Implements SAX interface"""
+ if "vocabulary" == name:
+ self.deck.updateProgress()
+ elif name.endswith("label"):
+ self.labels[name.replace("label", "")] = self.currentContent
+ elif name.startswith("vis"):
+ self.visibility[name.replace("vis", "")] = self.currentContent
+ elif "etai" == name:
+ self.attributeItems[self.eid] = self.currentContent
+ elif "etattributes" == name:
+ self.deck.updateProgress()
+ elif "entrytypes" == name:
+ self.deck.updateProgress()
+ elif "name" == name:
+ self.unitCategories[self.eid] = self.prepareTag(self.currentContent)
+ elif "units" == name:
+ self.deck.updateProgress()
+ elif "categories" == name:
+ self.deck.updateProgress()
+ elif "entries" == name:
+ self.deck.updateProgress()
+ elif "e" == name:
+ self.deck.addFact(self.f)
+ self.countFacts += 1
+ # there is a not logical mapping between the tags for fields and names in VocabInfo
+ # See net.vanosten.dings.consts.Constants.XML_*
+ elif "o" == name:
+ self.f.__setitem__(self.labels["b"], self.currentContent)
+ elif "d" == name:
+ self.f.__setitem__(self.labels["t"], self.currentContent)
+ elif "ep" == name:
+ self.f.__setitem__(self.labels["exp"], self.currentContent)
+ elif "ea" == name:
+ self.f.__setitem__(self.labels["ex"], self.currentContent)
+ elif "p" == name:
+ self.f.__setitem__(self.labels["pro"], self.currentContent)
+ elif "r" == name:
+ self.f.__setitem__(self.labels["rel"], self.currentContent)
+
+ def characters(self, content):
+ """Implements SAX interface"""
+ self.currentContent = content.strip()
+
+ def createModel(self, attrs):
+ """Makes a new Anki (fact) model from an entry type.
+ The card models are made each time from scratch in order that evt. model specific fields (attributes) can make part."""
+ m = Model(attrs["n"])
+ # field model for standard fields
+ m.addFieldModel(FieldModel(self.labels["b"], True, False)) #there is no uniqueness check in DingsBums?!
+ m.addFieldModel(FieldModel(self.labels["t"], True, False))
+ for aField in ["exp", "ex", "pro", "rel"]:
+ if self.visibility[aField] in "012":
+ m.addFieldModel(FieldModel(self.labels[aField], False, False))
+ # field models for attributes
+ for attr in ["a1", "a2" "a3", "a4"]:
+ if attr in attrs:
+ m.addFieldModel(FieldModel(self.attributes[attrs[attr]], False, False))
+ self.typeAttributes[attrs["eid"] + "_" + attr] = self.attributes[attrs[attr]]
+
+ # card model for front
+ frontStrings = ["%(" + self.labels["b"] + ")s"]
+ backStrings = ["%(" + self.labels["t"] + ")s"]
+ for aField in ["exp", "ex", "pro", "rel"]:
+ if self.visibility[aField] in "01":
+ frontStrings.append("%(" + self.labels[aField] + ")s")
+ if self.visibility[aField] in "02":
+ backStrings.append("%(" + self.labels[aField] + ")s")
+ m.addCardModel(CardModel(u'Forward', "
".join(frontStrings), "
".join(backStrings)))
+ # card model for back
+ m.addCardModel(CardModel(u'Reverse', unicode("%(" + self.labels["t"] + ")s"), unicode("%(" + self.labels["b"] + ")s")))
+ # tags is just the name without spaces
+ m.tags = self.prepareTag(m.name)
+
+ # link
+ self.models[attrs["eid"]] = m
+ self.deck.addModel(m)
+
+ def createFact(self, attrs):
+ """Makes a new Anki fact from an entry."""
+ model = self.models[attrs["et"]]
+ self.f = Fact(model)
+ # process attributes
+ for attr in ["a1", "a2" "a3", "a4"]:
+ if attr in attrs:
+ self.f.__setitem__(self.typeAttributes[attrs["et"] + "_" + attr], self.attributeItems[attrs[attr]])
+ # process tags. Unit, Category plus entry type name
+ tagString = unicode(self.unitCategories[attrs["u"]] + " " + self.unitCategories[attrs["c"]] + " " + model.tags)
+ self.f.tags = tagString
+
+ def prepareTag(self, stringWithSpace):
+ parts = stringWithSpace.split()
+ return "_".join(parts)
+
+class DingsBumsImporter(Importer):
+ needMapper = False # needs to overwrite default in Importer - otherwise Mapping dialog is shown in GUI
+
+ def __init__(self, deck, file):
+ Importer.__init__(self, deck, file)
+ self.deck = deck
+ self.file = file
+ self.total = 0
+
+ def doImport(self):
+ """Totally overrides the method in Importer"""
+ num = 7 # the number of updates to progress bar (see references in method endElement in DingsBumsHandler
+ self.deck.startProgress(num)
+ self.deck.updateProgress(_("Importing..."))
+
+ # parse the DingsBums?! xml file
+ handler = DingsBumsHandler(self.deck)
+ saxparser = make_parser( )
+ saxparser.setContentHandler(handler)
+ saxparser.parse(self.file)
+ self.total = handler.countFacts
+ self.deck.finishProgress()
+ self.deck.setModified()
+
+if __name__ == '__main__':
+ print "Starting ..."
+
+ # for testing you can start it standalone. Use an argument to specify the file to import
+ filename = str(sys.argv[1])
+
+ mydeck = DeckStorage.Deck()
+ i = DingsBumsImporter(mydeck, filename)
+ i.doImport()
+ assert 7 == i.total
+ mydeck.s.close()
+
+ print "... Finished"
+ sys.exit(1)
\ No newline at end of file
diff --git a/tests/importing/dingsbums.xml b/tests/importing/dingsbums.xml
new file mode 100644
index 000000000..a00bd17df
--- /dev/null
+++ b/tests/importing/dingsbums.xml
@@ -0,0 +1,50 @@
+
+
+Deutsch - ItalienischRick Gruber-RiemerKlett Grund- und AufbauwortschatzDeutschItalienischAttributesLektionKategoriOthersErklärungUnregelmässigen_USen_USen_USen_USen_USen_USen_USen_USen_US1000222falsefalse
+
+RegelmässigUnregelmässig
+avereessereavere oder essere
+-are (regelmässig)-ere (regelmässig)-ire (regelmässig)Unregelmässig
+illa
+RegelmässigUnregelmässigNur EinzahlNur Mehrzahl
+
+
+
+
+
+
+
+
+
+
+
+
+Rest
+Harenberg Kalender Italienisch 2007
+50. Restaurant, Café, Hotel
+Berlitz Kalender 2005
+A
+
+
+Default
+
+
+entfernen, beseitigenallontanare
+dann; damals, also; früherallora
+Schüler, Zöglingallievo
+lustig, heiterallegro
+sich in einer unbequemen Situation befindenessere un pesche four d' aqua
+das ist mir egalme ne fregoGeste: unter dem Kinn mit der Hand vonhinten nach vorne reiben
+Wirtinostessa
+
+
+2361915312
+1961915312
+1251251100
+2021915312
+1881251100
+1241811000
+1761915312
+1241811000
+
+
diff --git a/tests/test_importing.py b/tests/test_importing.py
index d79384d2f..6b986cda8 100644
--- a/tests/test_importing.py
+++ b/tests/test_importing.py
@@ -5,7 +5,7 @@ from tests.shared import assertException
from anki.errors import *
from anki import DeckStorage
-from anki.importing import anki10, csvfile, mnemosyne10, supermemo_xml
+from anki.importing import anki10, csvfile, mnemosyne10, supermemo_xml, dingsbums
from anki.stdmodels import BasicModel
from anki.facts import Fact
from anki.sync import SyncClient, SyncServer
@@ -131,3 +131,13 @@ def test_anki10_modtime():
assert deck2.s.scalar("select count(*) from cards") == 2
assert deck2.s.scalar("select count(*) from facts") == 2
assert deck2.s.scalar("select count(*) from models") == 2
+
+def test_dingsbums():
+ deck = DeckStorage.Deck()
+ deck.addModel(BasicModel())
+ startNumberOfFacts = deck.factCount
+ file = unicode(os.path.join(testDir, "importing/dingsbums.xml"))
+ i = dingsbums.DingsBumsImporter(deck, file)
+ i.doImport()
+ assert 7 == i.total
+ deck.s.close()