# -*- 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.keys():
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.keys():
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)