def rename(self, g, newName): "Rename deck prefix to NAME if not exists. Updates children." # make sure target node doesn't already exist if newName in self.allNames(): raise DeckRenameError(_("That deck already exists.")) # make sure we're not nesting under a filtered deck for p in self.parentsByName(newName): if p['dyn']: raise DeckRenameError( _("A filtered deck cannot have subdecks.")) # ensure we have parents newName = self._ensureParents(newName) # rename children for grp in self.all(): if grp['name'].startswith(g['name'] + "::"): grp['name'] = grp['name'].replace(g['name'] + "::", newName + "::", 1) self.save(grp) # adjust name g['name'] = newName # ensure we have parents again, as we may have renamed parent->child newName = self._ensureParents(newName) self.save(g) # renaming may have altered active did order self.maybeAddToActive()
def foreignNotes(self): self.open() # process all lines log = [] notes = [] lineNum = 0 ignored = 0 if self.delimiter: reader = csv.reader(self.data, delimiter=self.delimiter, doublequote=True) else: reader = csv.reader(self.data, self.dialect, doublequote=True) try: for row in reader: if len(row) != self.numFields: if row: log.append( _("'%(row)s' had %(num1)d fields, " "expected %(num2)d") % { "row": " ".join(row), "num1": len(row), "num2": self.numFields, }) ignored += 1 continue note = self.noteFromFields(row) notes.append(note) except (csv.Error) as e: log.append(_("Aborted: %s") % str(e)) self.log = log self.ignored = ignored self.fileobj.close() return notes
def _nextDueMsg(self): line = [] # the new line replacements are so we don't break translations # in a point release if self.revDue(): line.append( _("""\ Today's review limit has been reached, but there are still cards waiting to be reviewed. For optimum memory, consider increasing the daily limit in the options.""").replace("\n", " ")) if self.newDue(): line.append( _("""\ There are more new cards available, but the daily limit has been reached. You can increase the limit in the options, but please bear in mind that the more new cards you introduce, the higher your short-term review workload will become.""").replace("\n", " ")) if self.haveBuried(): if self.haveCustomStudy: now = " " + _( "To see them now, click the Unbury button below.") else: now = "" line.append( _("""\ Some related or buried cards were delayed until a later session.""") + now) if self.haveCustomStudy and not self.col.decks.current()['dyn']: line.append( _("""\ To study outside of the normal schedule, click the Custom Study button below.""" )) return "<p>".join(line)
def ivlGraph(self): (ivls, all, avg, max_), chunk = self._ivls() tot = 0 totd = [] if not ivls or not all: return "" for (grp, cnt) in ivls: tot += cnt totd.append((grp, tot/float(all)*100)) if self.type == 0: ivlmax = 31 elif self.type == 1: ivlmax = 52 else: ivlmax = max(5, ivls[-1][0]) txt = self._title(_("Intervals"), _("Delays until reviews are shown again.")) txt += self._graph(id="ivl", ylabel2=_("Percentage"), xunit=chunk, data=[ dict(data=ivls, color=colIvl), dict(data=totd, color=colCum, yaxis=2, bars={'show': False}, lines=dict(show=True), stack=False) ], conf=dict( xaxis=dict(min=-0.5, max=ivlmax+0.5), yaxes=[dict(), dict(position="right", max=105)])) i = [] self._line(i, _("Average interval"), fmtTimeSpan(avg*86400)) self._line(i, _("Longest interval"), fmtTimeSpan(max_*86400)) return txt + self._lineTbl(i)
def addForwardReverse(col): mm = col.models m = addBasicModel(col) m['name'] = _("Basic (and reversed card)") t = mm.newTemplate(_("Card 2")) t['qfmt'] = "{{" + _("Back") + "}}" t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n" + "{{" + _( "Front") + "}}" mm.addTemplate(m, t) return m
def _dueInfo(self, tot, num): i = [] self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot) self._line(i, _("Average"), self._avgDay( tot, num, _("reviews"))) tomorrow = self.col.db.scalar(""" select count() from cards where did in %s and queue in (2,3) and due = ?""" % self._limit(), self.col.sched.today+1) tomorrow = ngettext("%d card", "%d cards", tomorrow) % tomorrow self._line(i, _("Due tomorrow"), tomorrow) return self._lineTbl(i)
def _errMsg(type, texpath): msg = (_("Error executing %s.") % type) + "<br>" msg += (_("Generated file: %s") % texpath) + "<br>" try: with open(namedtmp("latex_log.txt", rm=False)) as f: log = f.read() if not log: raise Exception() msg += "<small><pre>" + html.escape(log) + "</pre></small>" except: msg += _("Have you installed latex and dvipng/dvisvgm?") return msg
def addForwardOptionalReverse(col): mm = col.models m = addBasicModel(col) m['name'] = _("Basic (optional reversed card)") av = _("Add Reverse") fm = mm.newField(av) mm.addField(m, fm) t = mm.newTemplate(_("Card 2")) t['qfmt'] = "{{#%s}}{{%s}}{{/%s}}" % (av, _("Back"), av) t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n" + "{{" + _( "Front") + "}}" mm.addTemplate(m, t) return m
def dynOrderLabels(): return { 0: _("Oldest seen first"), 1: _("Random"), 2: _("Increasing intervals"), 3: _("Decreasing intervals"), 4: _("Most lapses"), 5: _("Order added"), 6: _("Order due"), 7: _("Latest added first"), 8: _("Relative overdueness"), }
def _avgDay(self, tot, num, unit): vals = [] try: vals.append(_("%(a)0.1f %(b)s/day") % dict(a=tot/float(num), b=unit)) return ", ".join(vals) except ZeroDivisionError: return ""
class AnkiCollectionPackageExporter(AnkiPackageExporter): key = _("Anki Collection Package") ext = ".colpkg" verbatim = True includeSched = None def __init__(self, col): AnkiPackageExporter.__init__(self, col) def doExport(self, z, path): # close our deck & write it into the zip file, and reopen self.count = self.col.cardCount() v2 = self.col.schedVer() != 1 self.col.close() if not v2: z.write(self.col.path, "collection.anki_lib.") else: self._addDummyCollection(z) z.write(self.col.path, "collection.anki_lib.1") self.col.reopen() # copy all media if not self.includeMedia: return {} mdir = self.col.media.dir() return self._exportMedia(z, os.listdir(mdir), mdir)
class TextNoteExporter(Exporter): key = _("Notes in Plain Text") ext = ".txt" includeTags = True includeHTML = True def __init__(self, col): Exporter.__init__(self, col) self.includeID = False def doExport(self, file): cardIds = self.cardIds() data = [] for id, flds, tags in self.col.db.execute(""" select guid, flds, tags from notes where id in (select nid from cards where cards.id in %s)""" % ids2str(cardIds)): row = [] # note id if self.includeID: row.append(str(id)) # fields row.extend([self.processText(f) for f in splitFields(flds)]) # tags if self.includeTags: row.append(tags.strip()) data.append("\t".join(row)) self.count = len(data) out = "\n".join(data) file.write(out.encode("utf-8"))
def _renderQA(self, data, qfmt=None, afmt=None): "Returns hash of id, question, answer." # data is [cid, nid, mid, did, ord, tags, flds, cardFlags] # unpack fields and create dict flist = splitFields(data[6]) fields = {} model = self.models.get(data[2]) for (name, (idx, conf)) in list(self.models.fieldMap(model).items()): fields[name] = flist[idx] fields['Tags'] = data[5].strip() fields['Type'] = model['name'] fields['Deck'] = self.decks.name(data[3]) fields['Subdeck'] = fields['Deck'].split('::')[-1] fields['CardFlag'] = self._flagNameFromCardFlags(data[7]) if model['type'] == MODEL_STD: template = model['tmpls'][data[4]] else: template = model['tmpls'][0] fields['Card'] = template['name'] fields['c%d' % (data[4] + 1)] = "1" # render q & a d = dict(id=data[0]) qfmt = qfmt or template['qfmt'] afmt = afmt or template['afmt'] for (type, format) in (("q", qfmt), ("a", afmt)): if type == "q": format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4] + 1), format) format = format.replace("<%cloze:", "<%%cq:%d:" % (data[4] + 1)) else: format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4] + 1), format) format = format.replace("<%cloze:", "<%%ca:%d:" % (data[4] + 1)) fields['FrontSide'] = stripSounds(d['q']) fields = runFilter("mungeFields", fields, model, data, self) html = anki_lib.template.render(format, fields) d[type] = runFilter("mungeQA", html, type, fields, model, data, self) # empty cloze? if type == 'q' and model['type'] == MODEL_CLOZE: if not self.models._availClozeOrds(model, data[6], False): d['q'] += ("<p>" + _( "Please edit this note and add some cloze deletions. (%s)" ) % ("<a href=%s#cloze>%s</a>" % (HELP_SITE, _("help")))) return d
def markReview(self, card): old = [] if self._undo: if self._undo[0] == 1: old = self._undo[2] self.clearUndo() wasLeech = card.note().hasTag("leech") or False self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech]
def emptyCardReport(self, cids): rep = "" for ords, cnt, flds in self.db.all(""" select group_concat(ord+1), count(), flds from cards c, notes n where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)): rep += _("Empty card numbers: %(c)s\nFields: %(f)s\n\n") % dict( c=ords, f=flds.replace("\x1f", " / ")) return rep
def introductionGraph(self): start, days, chunk = self.get_start_end_chunk() data = self._added(days, chunk) if not data: return "" conf = dict( xaxis=dict(tickDecimals=0, max=0.5), yaxes=[dict(min=0), dict(position="right", min=0)]) if days is not None: # pylint: disable=invalid-unary-operand-type conf['xaxis']['min'] = -days+0.5 def plot(id, data, ylabel, ylabel2): return self._graph( id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2) # graph repdata, repsum = self._splitRepData(data, ((1, colLearn, ""),)) txt = self._title( _("Added"), _("The number of new cards you have added.")) txt += plot("intro", repdata, ylabel=_("Cards"), ylabel2=_("Cumulative Cards")) # total and per day average tot = sum([i[1] for i in data]) period = self._periodDays() if not period: # base off date of earliest added card period = self._deckAge('add') i = [] self._line(i, _("Total"), ngettext("%d card", "%d cards", tot) % tot) self._line(i, _("Average"), self._avgDay(tot, period, _("cards"))) txt += self._lineTbl(i) return txt
def nextIvlStr(self, card, ease, short=False): "Return the next interval for CARD as a string." ivl = self.nextIvl(card, ease) if not ivl: return _("(end)") s = fmtTimeSpan(ivl, short=short) if ivl < self.col.conf['collapseTime']: s = "<" + s return s
def logger(self, text, level=1): "Wrapper for Anki logger" dLevels = {0: '', 1: 'Info', 2: 'Verbose', 3: 'Debug'} if level <= self.META.loggerLevel: #self.deck.updateProgress(_(text)) if self.META.logToStdOutput: print(self.__class__.__name__ + " - " + dLevels[level].ljust(9) + ' -\t' + _(text))
def todayStats(self): b = self._title(_("Today")) # studied today lim = self._revlogLimit() if lim: lim = " and " + lim cards, thetime, failed, lrn, rev, relrn, filt = self.col.db.first(""" select count(), sum(time)/1000, sum(case when ease = 1 then 1 else 0 end), /* failed */ sum(case when type = 0 then 1 else 0 end), /* learning */ sum(case when type = 1 then 1 else 0 end), /* review */ sum(case when type = 2 then 1 else 0 end), /* relearn */ sum(case when type = 3 then 1 else 0 end) /* filter */ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000) cards = cards or 0 thetime = thetime or 0 failed = failed or 0 lrn = lrn or 0 rev = rev or 0 relrn = relrn or 0 filt = filt or 0 # studied def bold(s): return "<b>"+str(s)+"</b>" msgp1 = ngettext("<!--studied-->%d card", "<!--studied-->%d cards", cards) % cards if cards: b += _("Studied %(a)s %(b)s today (%(secs).1fs/card)") % dict( a=bold(msgp1), b=bold(fmtTimeSpan(thetime, unit=1, inTime=True)), secs=thetime/cards ) # again/pass count b += "<br>" + _("Again count: %s") % bold(failed) if cards: b += " " + _("(%s correct)") % bold( "%0.1f%%" %((1-failed/float(cards))*100)) # type breakdown b += "<br>" b += (_("Learn: %(a)s, Review: %(b)s, Relearn: %(c)s, Filtered: %(d)s") % dict(a=bold(lrn), b=bold(rev), c=bold(relrn), d=bold(filt))) # mature today mcnt, msum = self.col.db.first(""" select count(), sum(case when ease = 1 then 0 else 1 end) from revlog where lastIvl >= 21 and id > ?"""+lim, (self.col.sched.dayCutoff-86400)*1000) b += "<br>" if mcnt: b += _("Correct answers on mature cards: %(a)d/%(b)d (%(c).1f%%)") % dict( a=msum, b=mcnt, c=(msum / float(mcnt) * 100)) else: b += _("No mature cards were studied today.") else: b += _("No cards have been studied today.") return b
def _addDummyCollection(self, zip): path = namedtmp("dummy.anki_lib.") c = Collection(path) n = c.newNote() n[_('Front')] = "This file requires a newer version of Anki." c.addNote(n) c.save() c.close() zip.write(path, "collection.anki_lib.") os.unlink(path)
def _getColVars(db): import anki_lib.collection import anki_lib.decks g = copy.deepcopy(anki_lib.decks.defaultDeck) g['id'] = 1 g['name'] = _("Default") g['conf'] = 1 g['mod'] = intTime() gc = copy.deepcopy(anki_lib.decks.defaultConf) gc['id'] = 1 return g, gc, anki_lib.collection.defaultConf.copy()
def addBasicTypingModel(col): mm = col.models m = mm.new(_("Basic (type in the answer)")) fm = mm.newField(_("Front")) mm.addField(m, fm) fm = mm.newField(_("Back")) mm.addField(m, fm) t = mm.newTemplate(_("Card 1")) t['qfmt'] = "{{" + _("Front") + "}}\n\n{{type:" + _("Back") + "}}" t['afmt'] = "{{" + _("Front") + "}}\n\n<hr id=answer>\n\n{{type:" + _( "Back") + "}}" mm.addTemplate(m, t) mm.add(m) return m
def dueGraph(self): start, end, chunk = self.get_start_end_chunk() d = self._due(start, end, chunk) yng = [] mtr = [] tot = 0 totd = [] for day in d: yng.append((day[0], day[1])) mtr.append((day[0], day[2])) tot += day[1]+day[2] totd.append((day[0], tot)) data = [ dict(data=mtr, color=colMature, label=_("Mature")), dict(data=yng, color=colYoung, label=_("Young")), ] if len(totd) > 1: data.append( dict(data=totd, color=colCum, label=_("Cumulative"), yaxis=2, bars={'show': False}, lines=dict(show=True), stack=False)) txt = self._title( _("Forecast"), _("The number of reviews due in the future.")) xaxis = dict(tickDecimals=0, min=-0.5) if end is not None: xaxis['max'] = end-0.5 txt += self._graph( id="due", data=data, xunit=chunk, ylabel2=_("Cumulative Cards"), conf=dict( xaxis=xaxis, yaxes=[ dict(min=0), dict(min=0, tickDecimals=0, position="right")] ), ) txt += self._dueInfo(tot, len(totd)*chunk) return txt
def easeGraph(self): # 3 + 4 + 4 + spaces on sides and middle = 15 # yng starts at 1+3+1 = 5 # mtr starts at 5+4+1 = 10 d = {'lrn':[], 'yng':[], 'mtr':[]} types = ("lrn", "yng", "mtr") eases = self._eases() for (type, ease, cnt) in eases: if type == 1: ease += 5 elif type == 2: ease += 10 n = types[type] d[n].append((ease, cnt)) ticks = [[1,1],[2,2],[3,3], # [4,4] [6,1],[7,2],[8,3],[9,4], [11, 1],[12,2],[13,3],[14,4]] if self.col.schedVer() != 1: ticks.insert(3, [4,4]) txt = self._title(_("Answer Buttons"), _("The number of times you have pressed each button.")) txt += self._graph(id="ease", data=[ dict(data=d['lrn'], color=colLearn, label=_("Learning")), dict(data=d['yng'], color=colYoung, label=_("Young")), dict(data=d['mtr'], color=colMature, label=_("Mature")), ], type="bars", conf=dict( xaxis=dict(ticks=ticks, min=0, max=15)), ylabel=_("Answers")) txt += self._easeInfo(eases) return txt
def shortTimeFmt(type): return { "years": _("%sy"), "months": _("%smo"), "days": _("%sd"), "hours": _("%sh"), "minutes": _("%sm"), "seconds": _("%ss"), }[type]
def postprocess(self, encode=True): self.encode = encode for c in processingChain: #print c if not self.encode and c[0] == 'lame': continue try: cmd, env = _packagedCmd(c) ret = retryWait(subprocess.Popen(cmd, startupinfo=si, env=env)) except: ret = True finally: self.cleanup() if ret: raise Exception(_("Error running %s") % " ".join(cmd))
def addClozeModel(col): mm = col.models m = mm.new(_("Cloze")) m['type'] = MODEL_CLOZE txt = _("Text") fm = mm.newField(txt) mm.addField(m, fm) fm = mm.newField(_("Extra")) mm.addField(m, fm) t = mm.newTemplate(_("Cloze")) fmt = "{{cloze:%s}}" % txt m['css'] += """ .cloze { font-weight: bold; color: blue; } .nightMode .cloze { color: lightblue; }""" t['qfmt'] = fmt t['afmt'] = fmt + "<br>\n{{%s}}" % _("Extra") mm.addTemplate(m, t) mm.add(m) return m
def _upgradeClozeModel(col, m): m['type'] = MODEL_CLOZE # convert first template t = m['tmpls'][0] for type in 'qfmt', 'afmt': t[type] = re.sub("{{cloze:1:(.+?)}}", r"{{cloze:\1}}", t[type]) t['name'] = _("Cloze") # delete non-cloze cards for the model rem = [] for t in m['tmpls'][1:]: if "{{cloze:" not in t['qfmt']: rem.append(t) for r in rem: col.models.remTemplate(m, r) del m['tmpls'][1:] col.models._updateTemplOrds(m) col.models.save(m)
def footer(self): b = "<br><br><font size=1>" b += _("Generated on %s") % time.asctime(time.localtime(time.time())) b += "<br>" if self.wholeCollection: deck = _("whole collection") else: deck = self.col.decks.current()['name'] b += _("Scope: %s") % deck b += "<br>" b += _("Period: %s") % [ _("1 month"), _("1 year"), _("deck life") ][self.type] return b
def _buildImg(col, latex, fname, model): # add header/footer latex = (model["latexPre"] + "\n" + latex + "\n" + model["latexPost"]) # it's only really secure if run in a jail, but these are the most common tmplatex = latex.replace("\\includegraphics", "") for bad in ("\\write18", "\\readline", "\\input", "\\include", "\\catcode", "\\openout", "\\write", "\\loop", "\\def", "\\shipout"): # don't mind if the sequence is only part of a command bad_re = "\\" + bad + "[^a-zA-Z]" if re.search(bad_re, tmplatex): return _("""\ For security reasons, '%s' is not allowed on cards. You can still use \ it by placing the command in a different package, and importing that \ package in the LaTeX header instead.""") % bad # commands to use? if model.get("latexsvg", False): latexCmds = svgCommands ext = "svg" else: latexCmds = pngCommands ext = "png" # write into a temp file log = open(namedtmp("latex_log.txt"), "w") texpath = namedtmp("tmp.tex") texfile = open(texpath, "w", encoding="utf8") texfile.write(latex) texfile.close() mdir = col.media.dir() oldcwd = os.getcwd() png = namedtmp("tmp.%s" % ext) try: # generate png os.chdir(tmpdir()) for latexCmd in latexCmds: if call(latexCmd, stdout=log, stderr=log): return _errMsg(latexCmd[0], texpath) # add to media shutil.copyfile(png, os.path.join(mdir, fname)) return finally: os.chdir(oldcwd) log.close()