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