refactor features to use hooks, update stdmodels, update findTags()

- remove description from fields, cards and models
- remove features and use field names instead
This commit is contained in:
Damien Elmes 2008-12-03 19:22:15 +09:00
parent 97caa8119f
commit 21b59408cd
10 changed files with 166 additions and 323 deletions

View file

@ -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

View file

@ -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."

View file

@ -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())

View file

@ -2,9 +2,9 @@
# Copyright: Damien Elmes <anki@ichi2.net>
# 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)

View file

@ -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)

View file

@ -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):

View file

@ -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'<hr>%(Back)s'))
m.addCardModel(CardModel(u'Reverse', u'%(Back)s', u'<hr>%(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"<hr>%(Reading)s<br>%(Meaning)s"))
m.addCardModel(CardModel(u"Production",
u"%(Meaning)s",
u"%(Expression)s<br>%(Reading)s"))
m.addCardModel(CardModel(u"Recognition", _(
"Test your ability to recognize the target expression"),
u"%(Expression)s",
u"%(Reading)s<br>%(Meaning)s"))
m.features = u"Japanese"
u"<hr>%(Expression)s<br>%(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"<a href=\"http://kanji.koohii.com/study?framenum="
u"%(text:Heisig number)s\">%(Keyword)s</a><br>",
u"%(Kanji)s<br><table width=150><tr><td align=left>"
u"画数%(Stroke count)s</td><td align=right>"
u"%(Heisig number)s</td></tr></table>"))
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<br>%(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<br>%(Meaning)s"))
m.features = u"Cantonese"
u"<hr>%(Reading)s<br>%(Meaning)s"))
m.addCardModel(CardModel(u"Production",
u"%(Meaning)s",
u"<hr>%(Expression)s<br>%(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<br>%(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<br>%(Meaning)s"))
m.features = u"Mandarin"
u"<hr>%(Reading)s<br>%(Meaning)s"))
m.addCardModel(CardModel(u"Production",
u"%(Meaning)s",
u"<hr>%(Expression)s<br>%(Reading)s",
active=False))
m.tags = u"Mandarin"
return m
models['Mandarin'] = MandarinModel
models['Mandarin'] = MandarinModel

View file

@ -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()

View file

@ -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()

View file

@ -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())