From 21b59408cde661c4bb17a9728554d16b37ffcc27 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 3 Dec 2008 19:22:15 +0900 Subject: [PATCH] refactor features to use hooks, update stdmodels, update findTags() - remove description from fields, cards and models - remove features and use field names instead --- anki/deck.py | 1 + anki/facts.py | 10 +- anki/features/__init__.py | 56 +-------- anki/features/chinese/__init__.py | 73 +++++------- anki/features/japanese.py | 47 ++++---- anki/models.py | 22 ++-- anki/stdmodels.py | 181 ++++++++---------------------- anki/utils.py | 87 ++++++++------ tests/test_deck.py | 8 +- tests/test_stdmodels.py | 4 - 10 files changed, 166 insertions(+), 323 deletions(-) diff --git a/anki/deck.py b/anki/deck.py index 758246c4d..e77c26e37 100644 --- a/anki/deck.py +++ b/anki/deck.py @@ -20,6 +20,7 @@ from anki.history import CardHistoryEntry from anki.models import Model, CardModel, formatQA from anki.stats import dailyStats, globalStats, genToday from anki.fonts import toPlatformFont +import anki.features from operator import itemgetter from itertools import groupby diff --git a/anki/facts.py b/anki/facts.py index fd3faf89e..8e6d52d0c 100644 --- a/anki/facts.py +++ b/anki/facts.py @@ -13,7 +13,7 @@ from anki.db import * from anki.errors import * from anki.models import Model, FieldModel, fieldModelsTable, formatQA from anki.utils import genID -from anki.features import FeatureManager +from anki.hooks import runHook # Fields in a fact ########################################################################## @@ -121,12 +121,8 @@ class Fact(object): req += " and id != %s" % field.id return not s.scalar(req, val=field.value, fmid=field.fieldModel.id) - def onSubmit(self): - FeatureManager.run(self.model.features, "onSubmit", self) - - def onKeyPress(self, field, value): - FeatureManager.run(self.model.features, - "onKeyPress", self, field, value) + def focusLost(self, field): + runHook('fact.focusLost', self, field) def setModified(self, textChanged=False): "Mark modified and update cards." diff --git a/anki/features/__init__.py b/anki/features/__init__.py index 745be81c8..8251073d1 100644 --- a/anki/features/__init__.py +++ b/anki/features/__init__.py @@ -3,63 +3,9 @@ # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html """\ -Features - extensible features like auto-reading generation +Features =============================================================================== - -Features allow the deck to define specific features that are required, but -that can be resolved in real time. This includes things like automatic reading -generation, language-specific dictionary entries, etc. """ -from anki.lang import _ -from anki.errors import * -from anki.utils import findTag, parseTags - -class Feature(object): - - def __init__(self, tags=None, name="", description=""): - if not tags: - tags = [] - self.tags = tags - self.name = name - self.description = description - - def onSubmit(self, fact): - "Apply any last-minute modifications to FACT before addition." - pass - - def onKeyPress(self, fact): - "Apply any changes to fact as it's being edited for the first time." - pass - - def run(self, cmd, *args): - "Run CMD." - attr = getattr(self, cmd, None) - if attr: - attr(*args) - -class FeatureManager(object): - - features = {} - - def add(feature): - "Add a feature." - FeatureManager.features[feature.name] = feature - add = staticmethod(add) - - def run(tagstr, cmd, *args): - "Run CMD on all matching features in DLIST." - tags = parseTags(tagstr) - for (name, feature) in FeatureManager.features.items(): - for tag in tags: - if findTag(tag, feature.tags): - feature.run(cmd, *args) - break - run = staticmethod(run) - -# Add bundled features import japanese -FeatureManager.add(japanese.FuriganaGenerator()) import chinese -FeatureManager.add(chinese.CantoneseGenerator()) -FeatureManager.add(chinese.MandarinGenerator()) diff --git a/anki/features/chinese/__init__.py b/anki/features/chinese/__init__.py index ae9ada107..dda2e4e10 100644 --- a/anki/features/chinese/__init__.py +++ b/anki/features/chinese/__init__.py @@ -2,9 +2,9 @@ # Copyright: Damien Elmes # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html -import sys, os, pickle -from anki.features import Feature -from anki.utils import findTag, parseTags, stripHTML +import sys, os +from anki.utils import findTag, stripHTML +from anki.hooks import addHook from anki.db import * class UnihanController(object): @@ -43,48 +43,37 @@ class UnihanController(object): return m[0] return "{%s}" % (",".join(m)) -class ChineseGenerator(Feature): +# Hooks +########################################################################## + +class ChineseGenerator(object): def __init__(self): - self.expressionField = "Expression" - self.readingField = "Reading" + self.unihan = None - def lazyInit(self): - pass + def toReading(self, type, val): + if not self.unihan: + self.unihan = UnihanController(type) + else: + self.unihan.type = type + return self.unihan.reading(val) - def onKeyPress(self, fact, field, value): - if findTag("Reading source", parseTags(field.fieldModel.features)): - dst = None - for field in fact.fields: - if findTag("Reading destination", - parseTags(field.fieldModel.features)): - dst = field - break - if not dst: - return - self.lazyInit() - reading = self.unihan.reading(value) - if not fact[dst.name]: - fact[dst.name] = reading +unihan = ChineseGenerator() -class CantoneseGenerator(ChineseGenerator): +def onFocusLost(fact, field): + if field.name != "Expression": + return + if findTag("Cantonese", fact.model.tags): + type = "cantonese" + elif findTag("Mandarin", fact.model.tags): + type = "mandarin" + else: + return + try: + if fact['Reading']: + return + except: + return + fact['Reading'] = unihan.toReading(type, field.value) - def __init__(self): - ChineseGenerator.__init__(self) - self.tags = ["Cantonese"] - self.name = "Reading generation for Cantonese" - - def lazyInit(self): - if 'unihan' not in self.__dict__: - self.unihan = UnihanController("cantonese") - -class MandarinGenerator(ChineseGenerator): - - def __init__(self): - ChineseGenerator.__init__(self) - self.tags = ["Mandarin"] - self.name = "Reading generation for Mandarin" - - def lazyInit(self): - if 'unihan' not in self.__dict__: - self.unihan = UnihanController("mandarin") +addHook('fact.focusLost', onFocusLost) diff --git a/anki/features/japanese.py b/anki/features/japanese.py index e925dcb14..cb0e3f4e0 100644 --- a/anki/features/japanese.py +++ b/anki/features/japanese.py @@ -3,8 +3,8 @@ # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html import sys, os -from anki.features import Feature -from anki.utils import findTag, parseTags, stripHTML +from anki.utils import findTag, stripHTML +from anki.hooks import addHook class KakasiController(object): def __init__(self): @@ -80,28 +80,25 @@ class KakasiController(object): return True return False -class FuriganaGenerator(Feature): +# Hook +########################################################################## - def __init__(self): - self.tags = ["Japanese"] - self.name = "Furigana generation based on kakasi." - self.kakasi = KakasiController() - if not self.kakasi.available(): - self.kakasi = None +kakasi = KakasiController() +if not kakasi.available(): + kakasi = None - def onKeyPress(self, fact, field, value): - if self.kakasi and findTag("Reading source", - parseTags(field.fieldModel.features)): - reading = self.kakasi.toFurigana(value) - dst = None - for field in fact.fields: - if findTag("Reading destination", parseTags( - field.fieldModel.features)): - dst = field - break - if dst: - if not fact[dst.name]: - if self.kakasi.formatForKakasi(value) != reading: - fact[dst.name] = reading - else: - fact[dst.name] = u"" +def onFocusLost(fact, field): + if not kakasi: + return + if field.name != "Expression": + return + if not findTag("Japanese", fact.model.tags): + return + try: + if fact['Reading']: + return + except: + return + fact['Reading'] = kakasi.toFurigana(field.value) + +addHook('fact.focusLost', onFocusLost) diff --git a/anki/models.py b/anki/models.py index 088f192c6..8cfcaf6f4 100644 --- a/anki/models.py +++ b/anki/models.py @@ -37,10 +37,10 @@ fieldModelsTable = Table( Column('ordinal', Integer, nullable=False), Column('modelId', Integer, ForeignKey('models.id'), nullable=False), Column('name', UnicodeText, nullable=False), - Column('description', UnicodeText, nullable=False, default=u""), - Column('features', UnicodeText, nullable=False, default=u""), + Column('description', UnicodeText, nullable=False, default=u""), # obsolete + Column('features', UnicodeText, nullable=False, default=u""), # obsolete Column('required', Boolean, nullable=False, default=True), - Column('unique', Boolean, nullable=False, default=True), + Column('unique', Boolean, nullable=False, default=True), # sqlite keyword Column('numeric', Boolean, nullable=False, default=False), # display Column('quizFontFamily', UnicodeText), @@ -52,9 +52,8 @@ fieldModelsTable = Table( class FieldModel(object): "The definition of one field in a fact." - def __init__(self, name=u"", description=u"", required=True, unique=True): + def __init__(self, name=u"", required=True, unique=True): self.name = name - self.description = description self.required = required self.unique = unique self.id = genID() @@ -70,7 +69,7 @@ cardModelsTable = Table( Column('ordinal', Integer, nullable=False), Column('modelId', Integer, ForeignKey('models.id'), nullable=False), Column('name', UnicodeText, nullable=False), - Column('description', UnicodeText, nullable=False, default=u""), + Column('description', UnicodeText, nullable=False, default=u""), # obsolete Column('active', Boolean, nullable=False, default=True), # formats: question/answer/last(not used) Column('qformat', UnicodeText, nullable=False), @@ -99,10 +98,8 @@ cardModelsTable = Table( class CardModel(object): """Represents how to generate the front and back of a card.""" - def __init__(self, name=u"", description=u"", - qformat=u"q", aformat=u"a", active=True): + def __init__(self, name=u"", qformat=u"q", aformat=u"a", active=True): self.name = name - self.description = description self.qformat = qformat self.aformat = aformat self.active = active @@ -145,17 +142,16 @@ modelsTable = Table( Column('modified', Float, nullable=False, default=time.time), Column('tags', UnicodeText, nullable=False, default=u""), Column('name', UnicodeText, nullable=False), - Column('description', UnicodeText, nullable=False, default=u""), - Column('features', UnicodeText, nullable=False, default=u""), + Column('description', UnicodeText, nullable=False, default=u""), # obsolete + Column('features', UnicodeText, nullable=False, default=u""), # obsolete Column('spacing', Float, nullable=False, default=0.1), Column('initialSpacing', Float, nullable=False, default=600), Column('source', Integer, nullable=False, default=0)) class Model(object): "Defines the way a fact behaves, what fields it can contain, etc." - def __init__(self, name=u"", description=u""): + def __init__(self, name=u""): self.name = name - self.description = description self.id = genID() def setModified(self): diff --git a/anki/stdmodels.py b/anki/stdmodels.py index bfa050d35..2b38f7d29 100644 --- a/anki/stdmodels.py +++ b/anki/stdmodels.py @@ -28,179 +28,88 @@ def names(): ########################################################################## def BasicModel(): - m = Model(_('Basic'), - _('A basic flashcard with a front and a back.\n' - 'Questions are asked from front to back by default.\n\n' - 'Please consider customizing this model, rather than\n' - 'using it verbatim: field names like "expression" are\n' - 'clearer than "front" and "back", and will ensure\n' - 'that your entries are consistent.')) - m.addFieldModel(FieldModel(u'Front', _('A question.'), True, True)) - m.addFieldModel(FieldModel(u'Back', _('The answer.'), True, True)) - m.addCardModel(CardModel(u'Front to back', _('Front to back'), - u'%(Front)s', u'%(Back)s')) - m.addCardModel(CardModel(u'Back to front', _('Back to front'), - u'%(Back)s', u'%(Front)s', active=False)) + m = Model(_('Basic')) + m.addFieldModel(FieldModel(u'Front', True, True)) + m.addFieldModel(FieldModel(u'Back', True, True)) + m.addCardModel(CardModel(u'Forward', u'%(Front)s', u'
%(Back)s')) + m.addCardModel(CardModel(u'Reverse', u'%(Back)s', u'
%(Front)s', + active=False)) + m.tags = u"Basic" return m + models['Basic'] = BasicModel # Japanese ########################################################################## def JapaneseModel(): - m = Model(_("Japanese"), - _(""" -The reading field is automatically generated by default, -and shows the reading for the expression. For words that -are normally written in hiragana or katakana and don't -need a reading, you can put the word in the expression -field, and leave the reading field blank. A reading will -will not automatically be generated for words written -in only hiragana or katakana. - -Note that the automatic generation of meaning is not -perfect, and should be checked before adding cards.""".strip())) + m = Model(_("Japanese")) # expression - f = FieldModel(u'Expression', - _('A word or expression written in Kanji.'), True, True) + f = FieldModel(u'Expression', True, True) font = u"Mincho" f.quizFontSize = 72 f.quizFontFamily = font f.editFontFamily = font - f.features = u"Reading source" m.addFieldModel(f) # meaning - m.addFieldModel(FieldModel( - u'Meaning', - _('A description in your native language, or Japanese'), True, True)) + m.addFieldModel(FieldModel(u'Meaning', True, True)) # reading - f = FieldModel(u'Reading', u"", False, False) + f = FieldModel(u'Reading', False, False) f.quizFontFamily = font f.editFontFamily = font - f.features = u"Reading destination" m.addFieldModel(f) - m.addCardModel(CardModel(u"Production", _( - "Actively test your recall by producing the target expression"), + m.addCardModel(CardModel(u"Recognition", + u"%(Expression)s", + u"
%(Reading)s
%(Meaning)s")) + m.addCardModel(CardModel(u"Production", u"%(Meaning)s", - u"%(Expression)s
%(Reading)s")) - m.addCardModel(CardModel(u"Recognition", _( - "Test your ability to recognize the target expression"), - u"%(Expression)s", - u"%(Reading)s
%(Meaning)s")) - m.features = u"Japanese" + u"
%(Expression)s
%(Reading)s", + active=False)) m.tags = u"Japanese" return m + models['Japanese'] = JapaneseModel -# English -########################################################################## - -def EnglishModel(): - m = Model(_("English"), - _(""" -Enter the English expression you want to learn in the 'Expression' field. -Enter a description in Japanese or English in the 'Meaning' field.""".strip())) - m.addFieldModel(FieldModel(u'Expression')) - m.addFieldModel(FieldModel(u'Meaning')) - m.addCardModel(CardModel( - u"Production", _("From the meaning to the English expression."), - u"%(Meaning)s", u"%(Expression)s")) - m.addCardModel(CardModel( - u"Recognition", _("From the English expression to the meaning."), - u"%(Expression)s", u"%(Meaning)s", active=False)) - m.tags = u"English" - return m -models['English'] = EnglishModel - -# Heisig -########################################################################## - -def HeisigModel(): - m = Model(_("Heisig"), - _(""" -A format suitable for Heisig's "Remembering the Kanji". -You are tested from the keyword to the kanji. - -Layout of the test is based on the great work at -http://kanji.koohii.com/ - -The link in the question will list user-contributed -stories. A free login is required.""".strip())) - font = u"Mincho" - f = FieldModel(u'Kanji') - f.quizFontSize = 150 - f.quizFontFamily = font - f.editFontFamily = font - m.addFieldModel(f) - m.addFieldModel(FieldModel(u'Keyword')) - m.addFieldModel(FieldModel(u'Story', u"", False, False)) - m.addFieldModel(FieldModel(u'Stroke count', u"", False, False)) - m.addFieldModel(FieldModel(u'Heisig number', required=False)) - m.addFieldModel(FieldModel(u'Lesson number', u"", False, False)) - m.addCardModel(CardModel( - u"Production", _("From the keyword to the Kanji."), - u"%(Keyword)s
", - u"%(Kanji)s
" - u"画数%(Stroke count)s" - u"%(Heisig number)s
")) - m.tags = u"Heisig" - return m -models['Heisig'] = HeisigModel - -# Chinese: Mandarin & Cantonese +# Cantonese ########################################################################## def CantoneseModel(): - m = Model(_("Cantonese"), - u"") - f = FieldModel(u'Expression', - _('A word or expression written in Hanzi.')) + m = Model(_("Cantonese")) + f = FieldModel(u'Expression') f.quizFontSize = 72 - f.features = u"Reading source" m.addFieldModel(f) - m.addFieldModel(FieldModel( - u'Meaning', _('A description in your native language, or Cantonese'))) - f = FieldModel(u'Reading', u"", False, False) - f.features = u"Reading destination" - m.addFieldModel(f) - m.addCardModel(CardModel(u"Production", _( - "Actively test your recall by producing the target expression"), - u"%(Meaning)s", - u"%(Expression)s
%(Reading)s")) - m.addCardModel(CardModel(u"Recognition", _( - "Test your ability to recognize the target expression"), + m.addFieldModel(FieldModel(u'Meaning')) + m.addFieldModel(FieldModel(u'Reading', False, False)) + m.addCardModel(CardModel(u"Recognition", u"%(Expression)s", - u"%(Reading)s
%(Meaning)s")) - m.features = u"Cantonese" + u"
%(Reading)s
%(Meaning)s")) + m.addCardModel(CardModel(u"Production", + u"%(Meaning)s", + u"
%(Expression)s
%(Reading)s", + active=False)) m.tags = u"Cantonese" return m + models['Cantonese'] = CantoneseModel +# Mandarin +########################################################################## + def MandarinModel(): - m = Model(_("Mandarin"), - u"") - f = FieldModel(u'Expression', - _('A word or expression written in Hanzi.')) + m = Model(_("Mandarin")) + f = FieldModel(u'Expression') f.quizFontSize = 72 - f.features = u"Reading source" m.addFieldModel(f) - m.addFieldModel(FieldModel( - u'Meaning', _( - 'A description in your native language, or Mandarin'))) - f = FieldModel(u'Reading', u"", False, False) - f.features = u"Reading destination" - m.addFieldModel(f) - m.addCardModel(CardModel(u"Production", _( - "Actively test your recall by producing the target expression"), - u"%(Meaning)s", - u"%(Expression)s
%(Reading)s")) - m.addCardModel(CardModel(u"Recognition", _( - "Test your ability to recognize the target expression"), + m.addFieldModel(FieldModel(u'Meaning')) + m.addFieldModel(FieldModel(u'Reading', False, False)) + m.addCardModel(CardModel(u"Recognition", u"%(Expression)s", - u"%(Reading)s
%(Meaning)s")) - m.features = u"Mandarin" + u"
%(Reading)s
%(Meaning)s")) + m.addCardModel(CardModel(u"Production", + u"%(Meaning)s", + u"
%(Expression)s
%(Reading)s", + active=False)) m.tags = u"Mandarin" return m -models['Mandarin'] = MandarinModel +models['Mandarin'] = MandarinModel diff --git a/anki/utils.py b/anki/utils.py index 58d96dd9a..28607a821 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -8,7 +8,7 @@ Miscellaneous utilities """ __docformat__ = 'restructuredtext' -import re, os, random, time +import re, os, random, time, types try: import hashlib @@ -20,6 +20,9 @@ except ImportError: from anki.db import * from anki.lang import _, ngettext +# Time handling +############################################################################## + timeTable = { "years": lambda n: ngettext("%s year", "%s years", n), "months": lambda n: ngettext("%s month", "%s months", n), @@ -102,40 +105,8 @@ def _pluralCount(time): return 1 return 2 -def parseTags(tags): - "Parse a string and return a list of tags." - tags = tags.split(",") - tags = [tag.strip() for tag in tags if tag.strip()] - return tags - -def joinTags(tags): - return u", ".join(tags) - -def canonifyTags(tags): - "Strip leading/trailing/superfluous commas." - return joinTags(sorted(set(parseTags(tags)))) - -def findTag(tag, tags): - "True if TAG is in TAGS. Ignore case." - return tag.lower() in [t.lower() for t in tags] - -def addTags(tagstr, tags): - "Add tag if doesn't exist." - currentTags = parseTags(tags) - for tag in parseTags(tagstr): - if not findTag(tag, currentTags): - currentTags.append(tag) - return u", ".join(currentTags) - -def deleteTags(tagstr, tags): - "Delete tag if exists." - currentTags = parseTags(tags) - for tag in parseTags(tagstr): - try: - currentTags.remove(tag) - except ValueError: - pass - return u", ".join(currentTags) +# HTML +############################################################################## def stripHTML(s): s = re.sub("<.*?>", "", s) @@ -167,6 +138,9 @@ def tidyHTML(html): html = re.sub(u' +$', u'', html) return html +# IDs +############################################################################## + def genID(static=[]): "Generate a random, unique 64bit ID." # 23 bits of randomness, 41 bits of current time @@ -208,5 +182,48 @@ This is safe if you use sqlite primary key columns, which are guaranteed to be integers.""" return "(%s)" % ",".join([str(i) for i in ids]) +# Tags +############################################################################## + +def parseTags(tags): + "Parse a string and return a list of tags." + tags = tags.split(",") + tags = [tag.strip() for tag in tags if tag.strip()] + return tags + +def joinTags(tags): + return u", ".join(tags) + +def canonifyTags(tags): + "Strip leading/trailing/superfluous commas and duplicates." + return joinTags(sorted(set(parseTags(tags)))) + +def findTag(tag, tags): + "True if TAG is in TAGS. Ignore case." + if not isinstance(tags, types.ListType): + tags = parseTags(tags) + return tag.lower() in [t.lower() for t in tags] + +def addTags(tagstr, tags): + "Add tags if they don't exist." + currentTags = parseTags(tags) + for tag in parseTags(tagstr): + if not findTag(tag, currentTags): + currentTags.append(tag) + return joinTags(currentTags) + +def deleteTags(tagstr, tags): + "Delete tags if they don't exists." + currentTags = parseTags(tags) + for tag in parseTags(tagstr): + try: + currentTags.remove(tag) + except ValueError: + pass + return joinTags(currentTags) + +# Misc +############################################################################## + def checksum(data): return md5(data).hexdigest() diff --git a/tests/test_deck.py b/tests/test_deck.py index 803508b50..e079ae14d 100644 --- a/tests/test_deck.py +++ b/tests/test_deck.py @@ -118,12 +118,8 @@ def test_cardOrder(): f['Meaning'] = u'2' deck.addFact(f) card = deck.getCard() - # production should come first - assert card.cardModel.name == u"Production" - # if we rebuild the queue, it should be the same - deck.rebuildQueue() - card = deck.getCard() - assert card.cardModel.name == u"Production" + # recognition should come first + assert card.cardModel.name == u"Recognition" def test_modelAddDelete(): deck = DeckStorage.Deck() diff --git a/tests/test_stdmodels.py b/tests/test_stdmodels.py index 5ba33b71b..b1a434641 100644 --- a/tests/test_stdmodels.py +++ b/tests/test_stdmodels.py @@ -13,10 +13,6 @@ def test_stdmodels(): deck = DeckStorage.Deck() deck.addModel(JapaneseModel()) deck = DeckStorage.Deck() - deck.addModel(EnglishModel()) - deck = DeckStorage.Deck() - deck.addModel(HeisigModel()) - deck = DeckStorage.Deck() deck.addModel(CantoneseModel()) deck = DeckStorage.Deck() deck.addModel(MandarinModel())