diff --git a/anki/cards.py b/anki/cards.py index 290d20587..5e455899f 100644 --- a/anki/cards.py +++ b/anki/cards.py @@ -4,6 +4,7 @@ import time from anki.utils import intTime, hexifyID, timestampID +from anki.consts import * # Cards ########################################################################## @@ -129,7 +130,11 @@ lapses=?, left=?, odue=?, odid=?, did=? where id = ?""", return self.col.models.get(self.note().mid) def template(self): - return self.model()['tmpls'][self.ord] + m = self.model() + if m['type'] == MODEL_STD: + return self.model()['tmpls'][self.ord] + else: + return self.model()['tmpls'][0] def startTimer(self): self.timerStarted = time.time() diff --git a/anki/collection.py b/anki/collection.py index 8f7035c1a..36b95858c 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -266,9 +266,19 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", "Return (active), non-empty templates." model = note.model() avail = self.models.availOrds(model, joinFields(note.fields)) + return self._tmplsFromOrds(model, avail) + + def _tmplsFromOrds(self, model, avail): ok = [] - for t in model['tmpls']: - if t['ord'] in avail: + if model['type'] == MODEL_STD: + for t in model['tmpls']: + if t['ord'] in avail: + ok.append(t) + else: + # cloze - generate temporary templates from first + for ord in avail: + t = copy.copy(model['tmpls'][0]) + t['ord'] = ord ok.append(t) return ok @@ -304,20 +314,21 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", model = self.models.get(mid) avail = self.models.availOrds(model, flds) did = dids.get(nid) or model['did'] - for t in model['tmpls']: + # add any missing cards + for t in self._tmplsFromOrds(model, avail): doHave = nid in have and t['ord'] in have[nid] - # if have ord but empty, add cid to remove list - # (may not have nid if generating before any cards added) - if doHave and t['ord'] not in avail: - rem.append(have[nid][t['ord']]) - # if missing ord and is available, generate - if not doHave and t['ord'] in avail: + if not doHave: # we'd like to use the same due# as sibling cards, but we # can't retrieve that quickly, so we give it a new id # instead data.append((ts, nid, t['did'] or did, t['ord'], now, usn, self.nextID("pos"))) ts += 1 + # note any cards that need removing + if nid in have: + for ord, id in have[nid].items(): + if ord not in avail: + rem.append(id) # bulk update self.db.executemany(""" insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""", @@ -440,20 +451,28 @@ select id from notes where id in %s and id not in (select nid from cards)""" % flist = splitFields(data[6]) fields = {} model = self.models.get(data[2]) + firstName = None for (name, (idx, conf)) in self.models.fieldMap(model).items(): + if idx == 0: + firstName = name fields[name] = flist[idx] fields['Tags'] = data[5] fields['Type'] = model['name'] fields['Deck'] = self.decks.name(data[3]) - template = model['tmpls'][data[4]] + if model['type'] == MODEL_STD: + template = model['tmpls'][data[4]] + else: + template = model['tmpls'][0] fields['Card'] = template['name'] # 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:") + format = format.replace("{{Cloze}}", "{{cq:%d:%s}}" % ( + data[4]+1, firstName)) else: - format = format.replace("cloze:", "ca:") + format = format.replace("{{Cloze}}", "{{ca:%d:%s}}" % ( + data[4]+1, firstName)) fields = runFilter("mungeFields", fields, model, data, self) html = anki.template.render(format, fields) d[type] = runFilter( diff --git a/anki/consts.py b/anki/consts.py index e0fa80f8c..55c3f0df5 100644 --- a/anki/consts.py +++ b/anki/consts.py @@ -37,11 +37,15 @@ DYN_FAILED = 5 DYN_ADDED = 6 DYN_DUE = 7 +# model types +MODEL_STD = 0 +MODEL_CLOZE = 1 + # deck schema & syncing vars -SCHEMA_VERSION = 3 +SCHEMA_VERSION = 4 SYNC_ZIP_SIZE = int(2.5*1024*1024) SYNC_URL = os.environ.get("SYNC_URL") or "https://beta.ankiweb.net/sync/" -SYNC_VER = 1 +SYNC_VER = 2 # Labels ########################################################################## diff --git a/anki/models.py b/anki/models.py index bed4f3f19..0499d0371 100644 --- a/anki/models.py +++ b/anki/models.py @@ -28,6 +28,7 @@ defaultModel = { 'mod': 0, 'usn': 0, 'vers': [], + 'type': MODEL_STD, } defaultField = { @@ -425,34 +426,21 @@ select id from notes where mid = ?)""" % " ".join(map), ########################################################################## def _updateRequired(self, m): + if m['type'] == MODEL_CLOZE: + # nothing to do + return req = [] flds = [f['name'] for f in m['flds']] - cloze = False for t in m['tmpls']: ret = self._reqForTemplate(m, flds, t) - if ret[2]: - cloze = True - req.append((t['ord'], ret[0], ret[1], ret[2])) + req.append((t['ord'], ret[0], ret[1])) m['req'] = req - m['cloze'] = cloze def _reqForTemplate(self, m, flds, t): a = [] b = [] - cloze = "cloze" in t['qfmt'] - reqstrs = [] - if cloze: - # need a cloze-specific filler - cloze = "" - nums = re.findall("\{\{cloze:(\d+):", t['qfmt']) - for n in nums: - n = int(n) - cloze += "{{c%d::foo}}" % n - # record that we require a specific string for generation - reqstrs.append("{{c%d::" % n) - return 'all', [], reqstrs for f in flds: - a.append(cloze if cloze else "1") + a.append("1") b.append("") data = [1, 1, m['id'], 1, t['ord'], "", joinFields(a)] full = self.col._renderQA(data)['q'] @@ -472,7 +460,7 @@ select id from notes where mid = ?)""" % " ".join(map), if self.col._renderQA(data)['q'] == empty: req.append(i) if req: - return type, req, reqstrs + return type, req # if there are no required fields, switch to any mode type = 'any' req = [] @@ -483,15 +471,17 @@ select id from notes where mid = ?)""" % " ".join(map), # if not the same as empty, this field can make the card non-blank if self.col._renderQA(data)['q'] != empty: req.append(i) - return type, req, reqstrs + return type, req def availOrds(self, m, flds): "Given a joined field string, return available template ordinals." + if m['type'] == MODEL_CLOZE: + return self._availClozeOrds(m, flds) fields = {} for c, f in enumerate(splitFields(flds)): fields[c] = f.strip() avail = [] - for ord, type, req, reqstrs in m['req']: + for ord, type, req in m['req']: # unsatisfiable template if type == "none": continue @@ -514,18 +504,14 @@ select id from notes where mid = ?)""" % " ".join(map), break if not ok: continue - # extra cloze requirement? - ok = True - for s in reqstrs: - if s not in flds: - # required cloze string was missing - ok = False - break - if not ok: - continue avail.append(ord) return avail + def _availClozeOrds(self, m, flds): + ret = [int(m)-1 for m in re.findall( + "{{c(\d)::[^}]*?}}", splitFields(flds)[0])] + return list(set(ret)) + # Sync handling ########################################################################## diff --git a/anki/stdmodels.py b/anki/stdmodels.py index 6d61b293d..eb3931607 100644 --- a/anki/stdmodels.py +++ b/anki/stdmodels.py @@ -3,6 +3,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from anki.lang import _ +from anki.consts import MODEL_CLOZE models = [] @@ -31,20 +32,21 @@ models.append((_("Basic"), addBasicModel)) def addClozeModel(col): mm = col.models m = mm.new(_("Cloze")) - fm = mm.newField(("Text")) + m['type'] = MODEL_CLOZE + fm = mm.newField(_("Text")) mm.addField(m, fm) - for i in range(8): - n = i+1 - t = mm.newTemplate(_("Cloze") + " %d" % n) - fmt = "{{cloze:%d:Text}}" % n - t['css'] += """ + fm = mm.newField(_("Extra")) + mm.addField(m, fm) + t = mm.newTemplate(_("Cloze")) + fmt = "{{Cloze}}" + t['css'] += """ .cloze { font-weight: bold; color: blue; }""" - t['qfmt'] = fmt - t['afmt'] = fmt - mm.addTemplate(m, t) + t['qfmt'] = fmt + t['afmt'] = fmt + "
\n{{%s}}" % _("Extra") + mm.addTemplate(m, t) mm.add(m) return m diff --git a/anki/storage.py b/anki/storage.py index d4e4788e2..a646f1f5f 100644 --- a/anki/storage.py +++ b/anki/storage.py @@ -44,7 +44,6 @@ def Collection(path, lock=True, server=False, sync=True): col.lock() return col -# no upgrades necessary at the moment def _upgradeSchema(db): ver = db.scalar("select ver from col") if ver == SCHEMA_VERSION: @@ -81,6 +80,12 @@ def _upgrade(col, ver): d['dyn'] = 0 d['collapsed'] = False col.decks.save(d) + if ver < 4: + for m in col.models.all(): + if not "{{cloze::" in m['tmpls'][0]['qfmt']: + m['type'] = MODEL_STD + else: + pass # Creating a new collection ######################################################################