mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
refactor cloze deletions and text:field, add forecast
Previously cloze deletions were handled by copying the contents of one field into another and applying transforms to it. This had a number of problems: - after you add a card, you can't undo the cloze deletion - if you spot a mistake, you have to edit it twice (or more if you have more than one cloze for a sentence) - making multiple clozes requires copying & pasting the sentence multiple times - this also lead to much bigger decks if the sentences being cloze-deleted are large - related clozes can't be spaced apart as siblings To address these issues, we introduce the idea of cloze tags in the card template and fields. If the template has the text: {{cloze:1:field}} And a field has the following contents: {{c1::hello}} Then the template will automatically replace that part of the text with either occluded text, or a highlighted answer. All other clozes in the field are displayed normally. At the same time, we add support for text: into the template library, instead of manually creating text: fields in the dict for every field. Finally, add a forecast routine to get the due counts for the next week, which is used in the GUI.
This commit is contained in:
parent
8705085200
commit
ed75e4bee2
6 changed files with 145 additions and 17 deletions
|
@ -194,7 +194,7 @@ qconf=?, conf=?, data=?""",
|
||||||
# check we have card models available
|
# check we have card models available
|
||||||
cms = self.findTemplates(fact)
|
cms = self.findTemplates(fact)
|
||||||
if not cms:
|
if not cms:
|
||||||
return None
|
return 0
|
||||||
# flush the fact
|
# flush the fact
|
||||||
fact.id = self.nextID("fid")
|
fact.id = self.nextID("fid")
|
||||||
fact.flush()
|
fact.flush()
|
||||||
|
@ -430,13 +430,10 @@ select id from cards where fid in (select id from facts where mid = ?)""",
|
||||||
fields = {}
|
fields = {}
|
||||||
for (name, (idx, conf)) in model.fieldMap().items():
|
for (name, (idx, conf)) in model.fieldMap().items():
|
||||||
fields[name] = flist[idx]
|
fields[name] = flist[idx]
|
||||||
fields["text:"+name] = stripHTML(fields[name])
|
|
||||||
if fields[name]:
|
if fields[name]:
|
||||||
fields["text:"+name] = stripHTML(fields[name])
|
|
||||||
fields[name] = '<span class="fm%s-%s">%s</span>' % (
|
fields[name] = '<span class="fm%s-%s">%s</span>' % (
|
||||||
hexifyID(data[2]), hexifyID(idx), fields[name])
|
hexifyID(data[2]), hexifyID(idx), fields[name])
|
||||||
else:
|
else:
|
||||||
fields["text:"+name] = ""
|
|
||||||
fields[name] = ""
|
fields[name] = ""
|
||||||
fields['Tags'] = data[5]
|
fields['Tags'] = data[5]
|
||||||
fields['Model'] = model.name
|
fields['Model'] = model.name
|
||||||
|
@ -446,6 +443,10 @@ select id from cards where fid in (select id from facts where mid = ?)""",
|
||||||
# render q & a
|
# render q & a
|
||||||
d = dict(id=data[0])
|
d = dict(id=data[0])
|
||||||
for (type, format) in (("q", template['qfmt']), ("a", template['afmt'])):
|
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)
|
fields = runFilter("mungeFields", fields, model, gname, data, self)
|
||||||
html = anki.template.render(format, fields)
|
html = anki.template.render(format, fields)
|
||||||
d[type] = runFilter("mungeQA", html, fields, model, gname, data, self)
|
d[type] = runFilter("mungeQA", html, fields, model, gname, data, self)
|
||||||
|
|
|
@ -57,6 +57,17 @@ class Scheduler(object):
|
||||||
"Does not include fetched but unanswered."
|
"Does not include fetched but unanswered."
|
||||||
return (self.newCount, self.lrnCount, self.revCount)
|
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):
|
def countIdx(self, card):
|
||||||
return card.queue
|
return card.queue
|
||||||
|
|
||||||
|
|
|
@ -35,3 +35,30 @@ def BasicModel(deck):
|
||||||
return m
|
return m
|
||||||
|
|
||||||
models.append(BasicModel)
|
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}}<br>{{cloze:%d:%s}}<br>"+
|
||||||
|
"{{/cloze:%d:Text}}") % (n, n, _("Text"), n)
|
||||||
|
t['afmt'] = ("{{cloze:%d:" + _("Text") + "}}") % n
|
||||||
|
t['afmt'] += "<br>{{" + _("Notes") + "}}"
|
||||||
|
m.addTemplate(t)
|
||||||
|
return m
|
||||||
|
|
||||||
|
models.append(ClozeModel)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from anki.lang import _
|
||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
from anki.db import DB
|
from anki.db import DB
|
||||||
from anki.deck import _Deck
|
from anki.deck import _Deck
|
||||||
from anki.stdmodels import BasicModel
|
from anki.stdmodels import BasicModel, ClozeModel
|
||||||
from anki.errors import AnkiError
|
from anki.errors import AnkiError
|
||||||
|
|
||||||
def Deck(path, queue=True, lock=True):
|
def Deck(path, queue=True, lock=True):
|
||||||
|
@ -33,6 +33,9 @@ def Deck(path, queue=True, lock=True):
|
||||||
_upgradeDeck(deck, ver)
|
_upgradeDeck(deck, ver)
|
||||||
elif create:
|
elif create:
|
||||||
deck.addModel(BasicModel(deck))
|
deck.addModel(BasicModel(deck))
|
||||||
|
deck.addModel(ClozeModel(deck))
|
||||||
|
# default to basic
|
||||||
|
deck.conf['currentModelId'] = 1
|
||||||
deck.save()
|
deck.save()
|
||||||
if lock:
|
if lock:
|
||||||
deck.lock()
|
deck.lock()
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import re
|
import re
|
||||||
import cgi
|
import cgi
|
||||||
import collections
|
import collections
|
||||||
|
from anki.utils import stripHTML
|
||||||
|
|
||||||
|
clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}"
|
||||||
|
|
||||||
|
|
||||||
modifiers = {}
|
modifiers = {}
|
||||||
def modifier(symbol):
|
def modifier(symbol):
|
||||||
|
@ -79,7 +83,19 @@ class Template(object):
|
||||||
section, section_name, inner = match.group(0, 1, 2)
|
section, section_name, inner = match.group(0, 1, 2)
|
||||||
section_name = section_name.strip()
|
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 = ''
|
replacer = ''
|
||||||
# if it and isinstance(it, collections.Callable):
|
# if it and isinstance(it, collections.Callable):
|
||||||
# replacer = it(inner)
|
# replacer = it(inner)
|
||||||
|
@ -135,18 +151,39 @@ class Template(object):
|
||||||
@modifier(None)
|
@modifier(None)
|
||||||
def render_unescaped(self, tag_name=None, context=None):
|
def render_unescaped(self, tag_name=None, context=None):
|
||||||
"""Render a tag without escaping it."""
|
"""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('>')
|
# fixme: need a way to conditionally add these, so that including extra
|
||||||
# def render_partial(self, tag_name=None, context=None):
|
# fields in the question fmt doesn't make empty clozes
|
||||||
# """Renders a partial within the current context."""
|
def clozeText(self, txt, ord, type):
|
||||||
# # Import view here to avoid import loop
|
reg = clozeReg
|
||||||
# from pystache.view import View
|
m = re.search(reg%ord, txt)
|
||||||
|
if not m:
|
||||||
# view = View(context=context)
|
# cloze doesn't exist; return empty
|
||||||
# view.template_name = tag_name
|
return ""
|
||||||
|
# replace chosen cloze with type
|
||||||
# return view.render()
|
if type == "q":
|
||||||
|
if m.group(2):
|
||||||
|
txt = re.sub(reg%ord, "<b>...(\\3)</b>", txt)
|
||||||
|
else:
|
||||||
|
txt = re.sub(reg%ord, "<b>...</b>", txt)
|
||||||
|
else:
|
||||||
|
txt = re.sub(reg%ord, "<b>\\1</b>", txt)
|
||||||
|
# and display other clozes normally
|
||||||
|
return re.sub(reg%".*?", "\\1", txt)
|
||||||
|
|
||||||
@modifier('=')
|
@modifier('=')
|
||||||
def render_delimiter(self, tag_name=None, context=None):
|
def render_delimiter(self, tag_name=None, context=None):
|
||||||
|
|
|
@ -89,6 +89,55 @@ def test_templates():
|
||||||
assert c.ord == 0
|
assert c.ord == 0
|
||||||
stripHTML(c.q()) == "2"
|
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'hello<b>world'
|
||||||
|
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 <b>...</b>" in f.cards()[0].q()
|
||||||
|
assert "hello <b>world</b>" in f.cards()[0].a()
|
||||||
|
# and with a comment
|
||||||
|
f = d.newFact()
|
||||||
|
f['Text'] = "hello {{c1::world::typical}}"
|
||||||
|
assert d.addFact(f) == 1
|
||||||
|
assert "<b>...(typical)</b>" in f.cards()[0].q()
|
||||||
|
assert "<b>world</b>" 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 "<b>...</b> bar" in c1.q()
|
||||||
|
assert "<b>world</b> bar" in c1.a()
|
||||||
|
assert "world <b>...</b>" in c2.q()
|
||||||
|
assert "world <b>bar</b>" 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():
|
def test_modelChange():
|
||||||
print "model change"
|
print "model change"
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in a new issue