mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
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:
parent
97caa8119f
commit
21b59408cd
10 changed files with 166 additions and 323 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue