diff --git a/anki/deck.py b/anki/deck.py index 143dd15c9..6ba4b92c0 100644 --- a/anki/deck.py +++ b/anki/deck.py @@ -194,7 +194,7 @@ qconf=?, conf=?, data=?""", # check we have card models available cms = self.findTemplates(fact) if not cms: - return None + return 0 # flush the fact fact.id = self.nextID("fid") fact.flush() @@ -430,13 +430,10 @@ select id from cards where fid in (select id from facts where mid = ?)""", fields = {} for (name, (idx, conf)) in model.fieldMap().items(): fields[name] = flist[idx] - fields["text:"+name] = stripHTML(fields[name]) if fields[name]: - fields["text:"+name] = stripHTML(fields[name]) fields[name] = '%s' % ( hexifyID(data[2]), hexifyID(idx), fields[name]) else: - fields["text:"+name] = "" fields[name] = "" fields['Tags'] = data[5] fields['Model'] = model.name @@ -446,6 +443,10 @@ select id from cards where fid in (select id from facts where mid = ?)""", # render q & a d = dict(id=data[0]) for (type, format) in (("q", template['qfmt']), ("a", template['afmt'])): + if type == "q": + format = format.replace("cloze:", "cq:") + else: + format = format.replace("cloze:", "ca:") fields = runFilter("mungeFields", fields, model, gname, data, self) html = anki.template.render(format, fields) d[type] = runFilter("mungeQA", html, fields, model, gname, data, self) diff --git a/anki/sched.py b/anki/sched.py index cfcebe4a1..c6464ee8c 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -57,6 +57,17 @@ class Scheduler(object): "Does not include fetched but unanswered." return (self.newCount, self.lrnCount, self.revCount) + def dueForecast(self, days=7): + "Return counts over next DAYS. Includes today." + return self.db.list(""" +select count() from cards +where queue = 2 %s +and due between ? and ? +group by due +order by due""" % self._groupLimit("rev"), + self.today, + self.today+days-1) + def countIdx(self, card): return card.queue diff --git a/anki/stdmodels.py b/anki/stdmodels.py index 80092506f..fbbeb49ff 100644 --- a/anki/stdmodels.py +++ b/anki/stdmodels.py @@ -35,3 +35,30 @@ def BasicModel(deck): return m models.append(BasicModel) + +# Cloze +########################################################################## + +def ClozeModel(deck): + m = Model(deck) + m.name = _("Cloze") + fm = m.newField() + fm['name'] = _("Text") + fm['req'] = True + fm['uniq'] = True + m.addField(fm) + fm = m.newField() + fm['name'] = _("Notes") + m.addField(fm) + for i in range(8): + n = i+1 + t = m.newTemplate() + t['name'] = _("Cloze") + " %d" % n + t['qfmt'] = ("{{#cloze:%d:Text}}
{{cloze:%d:%s}}
"+ + "{{/cloze:%d:Text}}") % (n, n, _("Text"), n) + t['afmt'] = ("{{cloze:%d:" + _("Text") + "}}") % n + t['afmt'] += "
{{" + _("Notes") + "}}" + m.addTemplate(t) + return m + +models.append(ClozeModel) diff --git a/anki/storage.py b/anki/storage.py index da3a0fc68..8d87a6e6f 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -9,7 +9,7 @@ from anki.lang import _ from anki.utils import intTime from anki.db import DB from anki.deck import _Deck -from anki.stdmodels import BasicModel +from anki.stdmodels import BasicModel, ClozeModel from anki.errors import AnkiError def Deck(path, queue=True, lock=True): @@ -33,6 +33,9 @@ def Deck(path, queue=True, lock=True): _upgradeDeck(deck, ver) elif create: deck.addModel(BasicModel(deck)) + deck.addModel(ClozeModel(deck)) + # default to basic + deck.conf['currentModelId'] = 1 deck.save() if lock: deck.lock() diff --git a/anki/template/template.py b/anki/template/template.py index 531fcc6e0..34bc98378 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -1,6 +1,10 @@ import re import cgi import collections +from anki.utils import stripHTML + +clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}" + modifiers = {} def modifier(symbol): @@ -79,7 +83,19 @@ class Template(object): section, section_name, inner = match.group(0, 1, 2) section_name = section_name.strip() - it = get_or_attr(context, section_name, None) + # check for cloze + m = re.match("c[qa]:(\d+):(.+)", section_name) + if m: + # get full field text + txt = get_or_attr(context, m.group(2), None) + m = re.search(clozeReg%m.group(1), txt) + if m: + it = m.group(1) + else: + it = None + else: + it = get_or_attr(context, section_name, None) + replacer = '' # if it and isinstance(it, collections.Callable): # replacer = it(inner) @@ -135,18 +151,39 @@ class Template(object): @modifier(None) def render_unescaped(self, tag_name=None, context=None): """Render a tag without escaping it.""" - return unicode(get_or_attr(context, tag_name, '{unknown field %s}' % tag_name)) + if tag_name.startswith("text:"): + tag = tag_name[5:] + txt = get_or_attr(context, tag) + if txt: + return stripHTML(txt) + return "" + elif tag_name.startswith("cq:") or tag_name.startswith("ca:"): + m = re.match("c(.):(\d+):(.+)", tag_name) + (type, ord, tag) = (m.group(1), m.group(2), m.group(3)) + txt = get_or_attr(context, tag) + if txt: + return self.clozeText(txt, ord, type) + return "" + return get_or_attr(context, tag_name, '{unknown field %s}' % tag_name) - # @modifier('>') - # def render_partial(self, tag_name=None, context=None): - # """Renders a partial within the current context.""" - # # Import view here to avoid import loop - # from pystache.view import View - - # view = View(context=context) - # view.template_name = tag_name - - # return view.render() + # fixme: need a way to conditionally add these, so that including extra + # fields in the question fmt doesn't make empty clozes + def clozeText(self, txt, ord, type): + reg = clozeReg + m = re.search(reg%ord, txt) + if not m: + # cloze doesn't exist; return empty + return "" + # replace chosen cloze with type + if type == "q": + if m.group(2): + txt = re.sub(reg%ord, "...(\\3)", txt) + else: + txt = re.sub(reg%ord, "...", txt) + else: + txt = re.sub(reg%ord, "\\1", txt) + # and display other clozes normally + return re.sub(reg%".*?", "\\1", txt) @modifier('=') def render_delimiter(self, tag_name=None, context=None): diff --git a/tests/test_models.py b/tests/test_models.py index 22fc607d1..c4cf7a0f4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -89,6 +89,55 @@ def test_templates(): assert c.ord == 0 stripHTML(c.q()) == "2" +def test_text(): + d = getEmptyDeck() + m = d.currentModel() + m.templates[0]['qfmt'] = "{{text:Front}}" + m.flush() + f = d.newFact() + f['Front'] = u'helloworld' + d.addFact(f) + assert f.cards()[0].q() == "helloworld" + +def test_cloze(): + d = getEmptyDeck() + d.conf['currentModelId'] = 2 + f = d.newFact() + assert f.model().name == "Cloze" + # a cloze model with no clozes is empty + f['Text'] = u'nothing' + assert d.addFact(f) == 0 + # try with one cloze + f['Text'] = "hello {{c1::world}}" + assert d.addFact(f) == 1 + assert "hello ..." in f.cards()[0].q() + assert "hello world" in f.cards()[0].a() + # and with a comment + f = d.newFact() + f['Text'] = "hello {{c1::world::typical}}" + assert d.addFact(f) == 1 + assert "...(typical)" in f.cards()[0].q() + assert "world" in f.cards()[0].a() + # and with 2 clozes + f = d.newFact() + f['Text'] = "hello {{c1::world}} {{c2::bar}}" + assert d.addFact(f) == 2 + (c1, c2) = f.cards() + assert "... bar" in c1.q() + assert "world bar" in c1.a() + assert "world ..." in c2.q() + assert "world bar" in c2.a() + # clozes should be supported in sections too + m = d.currentModel() + m.templates[0]['qfmt'] = "{{#cloze:1:Text}}{{Notes}}{{/cloze:1:Text}}" + m.flush() + f = d.newFact() + f['Text'] = "hello" + f['Notes'] = "world" + assert d.addFact(f) == 0 + f['Text'] = "hello {{c1::foo}}" + assert d.addFact(f) == 1 + def test_modelChange(): print "model change" return