Example #1
0
    def nextDue(self, days=30):
        self.calcStats()
        fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
        graph = fig.add_subplot(111)
        dayslists = [self.stats['next'], self.stats['daysByType']['mature']]

        for dayslist in dayslists:
            self.addMissing(dayslist, self.stats['lowestInDay'], days)

        argl = []

        for dayslist in dayslists:
            dl = [x for x in dayslist.items() if x[0] <= days]
            argl.extend(list(self.unzip(dl)))

        self.filledGraph(graph, days, [dueYoungC, dueMatureC], *argl)

        cheat = fig.add_subplot(111)
        b1 = cheat.bar(0, 0, color = dueYoungC)
        b2 = cheat.bar(1, 0, color = dueMatureC)

        cheat.legend([b1, b2], [
            _("Young"),
            _("Mature")], loc='upper right')

        graph.set_xlim(xmin=self.stats['lowestInDay'], xmax=days)
        return fig
Example #2
0
 def showMapping(self, keepMapping=False, hook=None):
     if hook:
         hook()
     if not keepMapping:
         self.mapping = self.importer.mapping
     self.frm.mappingGroup.show()
     assert self.importer.fields()
     # set up the mapping grid
     if self.mapwidget:
         self.mapbox.removeWidget(self.mapwidget)
         self.mapwidget.deleteLater()
     self.mapwidget = QWidget()
     self.mapbox.addWidget(self.mapwidget)
     self.grid = QGridLayout(self.mapwidget)
     self.mapwidget.setLayout(self.grid)
     self.grid.setContentsMargins(3,3,3,3)
     self.grid.setSpacing(6)
     fields = self.importer.fields()
     for num in range(len(self.mapping)):
         text = _("Field <b>%d</b> of file is:") % (num + 1)
         self.grid.addWidget(QLabel(text), num, 0)
         if self.mapping[num] == "_tags":
             text = _("mapped to <b>Tags</b>")
         elif self.mapping[num]:
             text = _("mapped to <b>%s</b>") % self.mapping[num]
         else:
             text = _("<ignored>")
         self.grid.addWidget(QLabel(text), num, 1)
         button = QPushButton(_("Change"))
         self.grid.addWidget(button, num, 2)
         button.clicked.connect(lambda _, s=self,n=num: s.changeMappingNum(n))
Example #3
0
    def fixIntegrity(self):
        "Fix possible problems and rebuild caches."
        problems = []
        self.save()
        oldSize = os.stat(self.path)[stat.ST_SIZE]
        if self.db.scalar("pragma integrity_check") != "ok":
            return _("Collection is corrupt. Please see the manual.")
        # delete any notes with missing cards
        ids = self.db.list("""
select id from notes where id not in (select distinct nid from cards)""")
        self._remNotes(ids)
        # tags
        self.tags.registerNotes()
        # field cache
        for m in self.models.all():
            self.updateFieldCache(self.models.nids(m))
        # and finally, optimize
        self.optimize()
        newSize = os.stat(self.path)[stat.ST_SIZE]
        save = (oldSize - newSize)/1024
        txt = _("Database rebuilt and optimized.")
        if save > 0:
            txt += "\n" + _("Saved %dKB.") % save
        problems.append(txt)
        self.save()
        return "\n".join(problems)
Example #4
0
    def workDone(self, days=30):
        self.calcStats()

        for type in ["dayRepsNew", "dayRepsYoung", "dayRepsMature"]:
            self.addMissing(self.stats[type], -days, 0)

        fig = Figure(figsize=(self.width, self.height), dpi=self.dpi)
        graph = fig.add_subplot(111)

        args = sum((self.unzip(self.stats[type].items(), limit=days, reverseLimit=True) for type in ["dayRepsMature", "dayRepsYoung", "dayRepsNew"][::-1]), [])

        self.filledGraph(graph, days, [reviewNewC, reviewYoungC, reviewMatureC], *args)

        cheat = fig.add_subplot(111)
        b1 = cheat.bar(-3, 0, color = reviewNewC)
        b2 = cheat.bar(-4, 0, color = reviewYoungC)
        b3 = cheat.bar(-5, 0, color = reviewMatureC)

        cheat.legend([b1, b2, b3], [
            _("New"),
            _("Young"),
            _("Mature")], loc='upper left')

        graph.set_xlim(xmin=-days, xmax=0)
        graph.set_ylim(ymax=max(max(a for a in args[1::2])) + 10)

        return fig
Example #5
0
 def ivlGraph(self):
     (ivls, all, avg, max_) = 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] / 31)
     txt = self._title(_("Intervals"),
                       _("Delays until reviews are shown again."))
     txt += self._graph(id="ivl", ylabel2=_("Percentage"), 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)
Example #6
0
 def nextCard(self):
     elapsed = self.mw.col.timeboxReached()
     if elapsed:
         part1 = ngettext("%d card studied in", "%d cards studied in", elapsed[1]) % elapsed[1]
         mins = int(round(elapsed[0]/60))
         part2 = ngettext("%s minute.", "%s minutes.", mins) % mins
         fin = _("Finish")
         diag = askUserDialog("%s %s" % (part1, part2),
                          [_("Continue"), fin])
         diag.setIcon(QMessageBox.Information)
         if diag.run() == fin:
             return self.mw.moveToState("deckBrowser")
         self.mw.col.startTimebox()
     if self.cardQueue:
         # undone/edited cards to show
         c = self.cardQueue.pop()
         c.startTimer()
         self.hadCardQueue = True
     else:
         if self.hadCardQueue:
             # the undone/edited cards may be sitting in the regular queue;
             # need to reset
             self.mw.col.reset()
             self.hadCardQueue = False
         c = self.mw.col.sched.getCard()
     self.card = c
     clearAudioQueue()
     if not c:
         self.mw.moveToState("overview")
         return
     if self._reps is None or self._reps % 100 == 0:
         # we recycle the webview periodically so webkit can free memory
         self._initWeb()
     else:
         self._showQuestion()
Example #7
0
 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."))
     # ensure we have parents
     newName = self._ensureParents(newName)
     # make sure we're not nesting under a filtered deck
     if '::' in newName:
         newParent = '::'.join(newName.split('::')[:-1])
         if self.byName(newParent)['dyn']:
             raise DeckRenameError(_("A filtered deck cannot have subdecks."))
     # 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()
Example #8
0
 def foreignCards(self):
     self.sniff()
     # process all lines
     log = []
     cards = []
     lineNum = 0
     ignored = 0
     if self.delimiter:
         reader = csv.reader(self.data, delimiter=self.delimiter)
     else:
         reader = csv.reader(self.data, self.dialect)
     for row in reader:
         try:
             row = [unicode(x, "utf-8") for x in row]
         except UnicodeDecodeError, e:
             raise ImportFormatError(
                 type="encodingError",
                 info=_("The file was not in UTF8 format."))
         if len(row) != self.numFields:
             log.append(_(
                 "'%(row)s' had %(num1)d fields, "
                 "expected %(num2)d") % {
                 "row": u" ".join(row),
                 "num1": len(row),
                 "num2": self.numFields,
                 })
             ignored += 1
             continue
         card = self.cardFromFields(row)
         cards.append(card)
Example #9
0
    def _bottomHTML(self):
        return """
<table width=100%% cellspacing=0 cellpadding=0>
<tr>
<td align=left width=50 valign=top class=stat>
<br>
<button title="%(editkey)s" onclick="py.link('edit');">%(edit)s</button></td>
<td align=center valign=top id=middle>
</td>
<td width=50 align=right valign=top class=stat><span id=time class=stattxt>
</span><br>
<button onclick="py.link('more');">%(more)s &#9662;</button>
</td>
</tr>
</table>
<script>
var time = %(time)d;
var maxTime = 0;
$(function () {
$("#ansbut").focus();
updateTime();
setInterval(function () { time += 1; updateTime() }, 1000);
});

var updateTime = function () {
    if (!maxTime) {
        $("#time").text("");
        return;
    }
    time = Math.min(maxTime, time);
    var m = Math.floor(time / 60);
    var s = time %% 60;
    if (s < 10) {
        s = "0" + s;
    }
    var e = $("#time");
    if (maxTime == time) {
        e.html("<font color=red>" + m + ":" + s + "</font>");
    } else {
        e.text(m + ":" + s);
    }
}

function showQuestion(txt, maxTime_) {
  // much faster than jquery's .html()
  $("#middle")[0].innerHTML = txt;
  $("#ansbut").focus();
  time = 0;
  maxTime = maxTime_;
}

function showAnswer(txt) {
  $("#middle")[0].innerHTML = txt;
  $("#defease").focus();
}

</script>
""" % dict(rem=self._remaining(), edit=_("Edit"),
           editkey=_("Shortcut key: %s") % "E",
           more=_("More"), time=self.card.timeTaken() // 1000)
Example #10
0
    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)
Example #11
0
 def parseTopLine(self):
     "Parse the top line and determine the pattern and number of fields."
     # load & look for the right pattern
     self.cacheFile()
     # look for the first non-blank line
     l = None
     for line in self.lines:
         ret = line.strip()
         if ret:
             l = line
             break
     if not l:
         raise ImportFormatError(type="emptyFile",
                                 info=_("The file had no non-empty lines."))
     found = False
     for p in self.patterns:
         if p in l:
             pattern = p
             fields = l.split(p)
             numFields = len(fields)
             found = True
             break
     if not found:
         fmtError = _(
             "Couldn't find pattern. The file should be a series "
             "of lines separated by tabs or semicolons.")
         raise ImportFormatError(type="invalidPattern",
                                 info=fmtError)
     self.pattern = pattern
     self.setNumFields(line)
Example #12
0
 def showContextMenu(self):
     opts = [
         [_("Mark Note"), "*", self.onMark],
         [_("Bury Card"), "-", self.onBuryCard],
         [_("Bury Note"), "=", self.onBuryNote],
         [_("Suspend Card"), "@", self.onSuspendCard],
         [_("Suspend Note"), "!", self.onSuspend],
         [_("Delete Note"), "Delete", self.onDelete],
         [_("Options"), "O", self.onOptions],
         None,
         [_("Replay Audio"), "R", self.replayAudio],
         [_("Record Own Voice"), "Shift+V", self.onRecordVoice],
         [_("Replay Own Voice"), "V", self.onReplayRecorded],
     ]
     m = QMenu(self.mw)
     for row in opts:
         if not row:
             m.addSeparator()
             continue
         label, scut, func = row
         a = m.addAction(label)
         a.setShortcut(QKeySequence(scut))
         a.connect(a, SIGNAL("triggered()"), func)
     runHook("Reviewer.contextMenuEvent",self,m)
     m.exec_(QCursor.pos())
Example #13
0
File: editor.py Project: hans/anki
    def setupWeb(self):
        self.web = EditorWebView(self.widget, self)
        self.web.title = "editor"
        self.web.allowDrops = True
        self.web.onBridgeCmd = self.onBridgeCmd
        self.outerLayout.addWidget(self.web, 1)
        self.web.onLoadFinished = self._loadFinished

        topbuts = """
<div style="float:left;">
<button onclick="pycmd('fields')">%(flds)s...</button>
<button onclick="pycmd('cards')">%(cards)s...</button>
</div>
<div style="float:right;">
<button tabindex=-1 class=linkb type="button" id=bold onclick="pycmd('bold');return false;"><img class=topbut src="qrc:/icons/text_bold.png"></button>
<button tabindex=-1 class=linkb type="button" id=italic  onclick="pycmd('italic');return false;"><img class=topbut src="qrc:/icons/text_italic.png"></button>
<button tabindex=-1 class=linkb type="button" id=underline  onclick="pycmd('underline');return false;"><img class=topbut src="qrc:/icons/text_under.png"></button>
<button tabindex=-1 class=linkb type="button" id=superscript  onclick="pycmd('super');return false;"><img class=topbut src="qrc:/icons/text_super.png"></button>
<button tabindex=-1 class=linkb type="button" id=subscript  onclick="pycmd('sub');return false;"><img class=topbut src="qrc:/icons/text_sub.png"></button>
<button tabindex=-1 class=linkb type="button" onclick="pycmd('clear');return false;"><img class=topbut src="qrc:/icons/text_clear.png"></button>
<button tabindex=-1 class=linkb type="button" onclick="pycmd('colour');return false;"><div id=forecolor style="display:inline-block; background: #000;border-radius: 5px;" class=topbut></div></button>
<button tabindex=-1 class=linkb type="button" onclick="pycmd('changeCol');return false;"><div style="display:inline-block; border-radius: 5px;" class="topbut rainbow"></div></button>
<button tabindex=-1 class=linkb type="button" onclick="pycmd('cloze');return false;"><img class=topbut src="qrc:/icons/text_cloze.png"></button>
<button tabindex=-1 class=linkb type="button" onclick="pycmd('attach');return false;"><img class=topbut src="qrc:/icons/paperclip.png"></button>
<button tabindex=-1 class=linkb type="button" onclick="pycmd('record');return false;"><img class=topbut src="qrc:/icons/media-record.png"></button>
<button tabindex=-1 class=linkb type="button" onclick="pycmd('more');return false;"><img class=topbut src="qrc:/icons/more.png"></button>
</div>
        """ % dict(flds=_("Fields"), cards=_("Cards"))
        self.web.stdHtml(_html % (
            self.mw.baseHTML(), anki.js.jquery,
            topbuts,
            _("Show Duplicates")))
Example #14
0
    def importCards(self, cards):
        "Convert each card into a fact, apply attributes and add to deck."
        # ensure all unique and required fields are mapped
        for fm in self.model.fieldModels:
            if fm.required or fm.unique:
                if fm not in self.mapping:
                    raise ImportFormatError(
                        type="missingRequiredUnique",
                        info=_("Missing required/unique field '%(field)s'") %
                        {'field': fm.name})
        active = 0
        for cm in self.model.cardModels:
            if cm.active: active += 1
        if active > 1 and not self.multipleCardsAllowed:
            raise ImportFormatError(type="tooManyCards",
                info=_("""
The current importer only supports a single active card template. Please disable
all but one card template."""))
        # strip invalid cards
        cards = self.stripInvalid(cards)
        cards = self.stripOrTagDupes(cards)
        self.cardIds = []
        if cards:
            self.addCards(cards)
        return cards
Example #15
0
 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:
             row = [unicode(x, "utf-8") for x in row]
             if len(row) != self.numFields:
                 if row:
                     log.append(_(
                         "'%(row)s' had %(num1)d fields, "
                         "expected %(num2)d") % {
                         "row": u" ".join(row),
                         "num1": len(row),
                         "num2": self.numFields,
                         })
                     ignored += 1
                 continue
             note = self.noteFromFields(row)
             notes.append(note)
     except (csv.Error), e:
         log.append(_("Aborted: %s") % str(e))
Example #16
0
    def _table(self):
        counts = list(self.mw.col.sched.counts())
        finished = not sum(counts)
        if self.mw.col.schedVer() == 1:
            for n in range(len(counts)):
                if counts[n] >= 1000:
                    counts[n] = "1000+"
        but = self.mw.button
        if finished:
            return '<div style="white-space: pre-wrap;">%s</div>' % (
                self.mw.col.sched.finishedMsg())
        else:
            return '''
<table width=400 cellpadding=5>
<tr><td align=center valign=top>
<table cellspacing=5>
<tr><td>%s:</td><td><b><font color=#00a>%s</font></b></td></tr>
<tr><td>%s:</td><td><b><font color=#C35617>%s</font></b></td></tr>
<tr><td>%s:</td><td><b><font color=#0a0>%s</font></b></td></tr>
</table>
</td><td align=center>
%s</td></tr></table>''' % (
    _("New"), counts[0],
    _("Learning"), counts[1],
    _("To Review"), counts[2],
    but("study", _("Study Now"), id="study",extra=" autofocus"))
Example #17
0
    def onUnbury(self):
        if self.mw.col.schedVer() == 1:
            self.mw.col.sched.unburyCardsForDeck()
            self.mw.reset()
            return

        sibs = self.mw.col.sched.haveBuriedSiblings()
        man = self.mw.col.sched.haveManuallyBuried()

        if sibs and man:
            opts = [_("Manually Buried Cards"),
                    _("Buried Siblings"),
                    _("All Buried Cards"),
                    _("Cancel")]

            diag = askUserDialog(_("What would you like to unbury?"), opts)
            diag.setDefault(0)
            ret = diag.run()
            if ret == opts[0]:
                self.mw.col.sched.unburyCardsForDeck(type="manual")
            elif ret == opts[1]:
                self.mw.col.sched.unburyCardsForDeck(type="siblings")
            elif ret == opts[2]:
                self.mw.col.sched.unburyCardsForDeck(type="all")
        else:
            self.mw.col.sched.unburyCardsForDeck(type="all")

        self.mw.reset()
Example #18
0
    def _bottomHTML(self):
        return """
<center id=outer>
<table id=innertable width=100%% cellspacing=0 cellpadding=0>
<tr>
<td align=left width=50 valign=top class=stat>
<br>
<button title="%(editkey)s" onclick="pycmd('edit');">%(edit)s</button></td>
<td align=center valign=top id=middle>
</td>
<td width=50 align=right valign=top class=stat><span id=time class=stattxt>
</span><br>
<button onclick="pycmd('more');">%(more)s %(downArrow)s</button>
</td>
</tr>
</table>
</center>
<script>
time = %(time)d;
</script>
""" % dict(rem=self._remaining(), edit=_("Edit"),
           editkey=_("Shortcut key: %s") % "E",
           more=_("More"),
           downArrow=downArrow(),
           time=self.card.timeTaken() // 1000)
Example #19
0
def import_from_json():
    path = getFile(mw, "Org file to import", cb=None, dir=expanduser("~"))
    if not path:
        return
    with open(path, 'r') as f:
        content = f.read().decode('utf-8')

    entries = json.loads(content)
    import itertools
    get_deck = lambda e: e['deck']
    entries = sorted(entries, key=get_deck)

    mw.checkpoint(_("Import"))
    logs = []
    for deck_name, entries in itertools.groupby(entries, get_deck):
        # FIXME: If required we could group by model name also!
        importer = JsonImporter(mw.col, path, MODEL_NAME, deck_name)
        importer.initMapping()
        importer.run(list(entries))
        if importer.log:
            logs.append('\n'.join(importer.log))

    txt = _("Importing complete.") + "\n"
    txt += '\n'.join(logs)
    showText(txt)
    mw.reset()
Example #20
0
 def __init__(self, app, profileManager, opts, args):
     QMainWindow.__init__(self)
     self.state = "startup"
     self.opts = opts
     aqt.mw = self
     self.app = app
     self.pm = profileManager
     # init rest of app
     self.safeMode = self.app.queryKeyboardModifiers() & Qt.ShiftModifier
     try:
         self.setupUI()
         self.setupAddons()
     except:
         showInfo(_("Error during startup:\n%s") % traceback.format_exc())
         sys.exit(1)
     # must call this after ui set up
     if self.safeMode:
         tooltip(_("Shift key was held down. Skipping automatic "
                 "syncing and add-on loading."))
     # were we given a file to import?
     if args and args[0]:
         self.onAppMsg(args[0])
     # Load profile in a timer so we can let the window finish init and not
     # close on profile load error.
     self.progress.timer(10, self.setupProfile, False, requiresCollection=False)
Example #21
0
    def _unloadCollection(self):
        if not self.col:
            return
        if self.restoringBackup:
            label = _("Closing...")
        else:
            label = _("Backing Up...")
        self.progress.start(label=label, immediate=True)
        corrupt = False
        try:
            self.maybeOptimize()
            if not devMode:
                corrupt = self.col.db.scalar("pragma integrity_check") != "ok"
        except:
            corrupt = True
        try:
            self.col.close()
        except:
            corrupt = True
        finally:
            self.col = None
        if corrupt:
            showWarning(_("Your collection file appears to be corrupt. \
This can happen when the file is copied or moved while Anki is open, or \
when the collection is stored on a network or cloud drive. If problems \
persist after restarting your computer, please open an automatic backup \
from the profile screen."))
        if not corrupt and not self.restoringBackup:
            self.backup()

        self.progress.finish()
Example #22
0
 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 _update(self):
        if not self.shown:
            return
        txt = ""
        r = self.mw.reviewer
        d = self.mw.col
        cs = CardStats(d, r.card)
        cc = r.card
        if cc:
            txt += _("<h3>Current</h3>")
            txt += d.cardStats(cc)
            txt += "<p>"
            txt += self._revlogData(cc, cs)
        lc = r.lastCard()
        if lc:
            txt += _("<h3>Last</h3>")
            txt += d.cardStats(lc)
            txt += "<p>"
            txt += self._revlogData(lc, cs)
        if not txt:
            txt = _("No current card or last card.")
        style = self._style()
        self.web.setHtml("""
<html><head>
</head><style>%s</style>
<body><center>%s</center></body></html>"""% (style, txt))
Example #24
0
 def downloadIds(self, ids):
     log = []
     errs = []
     self.mw.progress.start(immediate=True)
     for n in ids:
         ret = download(self.mw, n)
         if ret[0] == "error":
             errs.append(_("Error downloading %(id)s: %(error)s") % dict(id=n, error=ret[1]))
             continue
         data, fname = ret
         fname = fname.replace("_", " ")
         name = os.path.splitext(fname)[0]
         ret = self.install(io.BytesIO(data),
                            manifest={"package": str(n), "name": name,
                                      "mod": intTime()})
         if ret[0] is False:
             if ret[1] == "conflicts":
                 continue
             if ret[1] == "zip":
                 showWarning(_("The download was corrupt. Please try again."))
             elif ret[1] == "manifest":
                 showWarning(_("Invalid add-on manifest."))
         log.append(_("Downloaded %(fname)s" % dict(fname=name)))
     self.mw.progress.finish()
     return log, errs
Example #25
0
def add_nids_to_all():
    """Add note id to all empty fields with the right names.

    Iterate over all notes and add the nid minus
    1’300’000’000’000. The subtraction is done mostly for aesthetical
    reasons.
    """
    if not askUser(
            _("Add note id to all “{fn}” fields?".format(
                fn=config["NoteIdFieldName"]))):
        return
    # Maybe there is a way to just select the notes which have a nid
    # field. But this should work and efficency isn't too much of an
    # issue.
    nids = mw.col.db.list("select id from notes")
    # Iterate over the cards
    for nid in progress(nids, _("Adding note ids."), _("Stop that!")):
        n = mw.col.getNote(nid)
        # Go over the fields ...
        for name in mw.col.models.fieldNames(n.model()):
            # ... and the target field names ..
            if name == config["NoteIdFieldName"]:
                # Check if target is empty
                if not n[name]:
                    n[name] = str(nid - int(15e11))
                    n.flush()
    mw.reset()
Example #26
0
 def __init__(self, mw, parent=None):
     self.parent = parent or mw
     self.mw = mw
     self.col = mw.col
     QDialog.__init__(self, self.parent, Qt.Window)
     self.model = None
     self.dialog = aqt.forms.addmodel.Ui_Dialog()
     self.dialog.setupUi(self)
     # standard models
     self.models = []
     for (name, func) in stdmodels.models:
         if isinstance(name, collections.Callable):
             name = name()
         item = QListWidgetItem(_("Add: %s") % name)
         self.dialog.models.addItem(item)
         self.models.append((True, func))
     # add copies
     for m in sorted(self.col.models.all(), key=itemgetter("name")):
         item = QListWidgetItem(_("Clone: %s") % m['name'])
         self.dialog.models.addItem(item)
         self.models.append((False, m))
     self.dialog.models.setCurrentRow(0)
     # the list widget will swallow the enter key
     s = QShortcut(QKeySequence("Return"), self)
     s.activated.connect(self.accept)
     # help
     self.dialog.buttonBox.helpRequested.connect(self.onHelp)
Example #27
0
    def onTimeout(self):
        error = cgi.escape(self.pool)
        self.pool = ""
        self.mw.progress.clear()
        if "abortSchemaMod" in error:
            return
        if "Pyaudio not" in error:
            return showWarning(_("Please install PyAudio"))
        if "install mplayer" in error:
            return showWarning(_("Please install mplayer"))
        if "no default output" in error:
            return showWarning(_("Please connect a microphone, and ensure "
                                 "other programs are not using the audio device."))
        if "invalidTempFolder" in error:
            return showWarning(self.tempFolderMsg())
        stdText = _("""\
An error occurred. It may have been caused by a harmless bug, <br>
or your deck may have a problem.
<p>To confirm it's not a problem with your deck, please run
<b>Tools &gt; Check Database</b>.
<p>If that doesn't fix the problem, please copy the following<br>
into a bug report:""")
        pluginText = _("""\
An error occurred in an add-on.<br>
Please post on the add-on forum:<br>%s<br>""")
        pluginText %= "https://anki.tenderapp.com/discussions/add-ons"
        if "addon" in error:
            txt = pluginText
        else:
            txt = stdText
        # show dialog
        txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>"
        showText(txt, type="html")
Example #28
0
    def __init__(self, mw, first=False, search="", deck=None):
        QDialog.__init__(self, mw)
        self.mw = mw
        self.deck = deck or self.mw.col.decks.current()
        self.search = search
        self.form = aqt.forms.dyndconf.Ui_Dialog()
        self.form.setupUi(self)
        if first:
            label = _("Build")
        else:
            label = _("Rebuild")
        self.ok = self.form.buttonBox.addButton(
            label, QDialogButtonBox.AcceptRole)
        self.mw.checkpoint(_("Options"))
        self.setWindowModality(Qt.WindowModal)
        self.form.buttonBox.helpRequested.connect(lambda: openHelp("filtered"))
        self.setWindowTitle(_("Options for %s") % self.deck['name'])
        restoreGeom(self, "dyndeckconf")
        self.initialSetup()
        self.loadConf()
        if search:
            self.form.search.setText(search + " is:due")
            self.form.search_2.setText(search + " is:new")
        self.form.search.selectAll()

        if self.mw.col.schedVer() == 1:
            self.form.secondFilter.setVisible(False)

        self.show()
        self.exec_()
        saveGeom(self, "dyndeckconf")
Example #29
0
    def fixIntegrity(self):
        "Fix possible problems and rebuild caches."
        problems = []
        self.save()
        oldSize = os.stat(self.path)[stat.ST_SIZE]
        if self.db.scalar("pragma integrity_check") != "ok":
            return _("Collection is corrupt. Please see the manual.")
        # delete any notes with missing cards
        ids = self.db.list("""
select id from notes where id not in (select distinct nid from cards)""")
        self._remNotes(ids)
        # tags
        self.tags.registerNotes()
        # field cache
        for m in self.models.all():
            self.updateFieldCache(self.models.nids(m))
        # new card position
        self.conf['nextPos'] = self.db.scalar(
            "select max(due)+1 from cards where type = 0") or 0
        # reviews should have a reasonable due #
        ids = self.db.list(
            "select id from cards where queue = 2 and due > 10000")
        if ids:
            problems.append("Reviews had incorrect due date.")
            self.db.execute(
                "update cards set due = 0, mod = ?, usn = ? where id in %s"
                % ids2str(ids), intTime(), self.usn())
        # and finally, optimize
        self.optimize()
        newSize = os.stat(self.path)[stat.ST_SIZE]
        txt = _("Database rebuilt and optimized.")
        ok = not problems
        problems.append(txt)
        self.save()
        return ("\n".join(problems), ok)
Example #30
0
    def onAppMsg(self, buf):
        if self.state == "startup":
            # try again in a second
            return self.progress.timer(1000, lambda: self.onAppMsg(buf), False)
        elif self.state == "profileManager":
            # can't raise window while in profile manager
            if buf == "raise":
                return
            self.pendingImport = buf
            return tooltip(_("Deck will be imported when a profile is opened."))
        if not self.interactiveState() or self.progress.busy():
            # we can't raise the main window while in profile dialog, syncing, etc
            if buf != "raise":
                showInfo(_("""\
Please ensure a profile is open and Anki is not busy, then try again."""),
                     parent=None)
            return
        # raise window
        if isWin:
            # on windows we can raise the window by minimizing and restoring
            self.showMinimized()
            self.setWindowState(Qt.WindowActive)
            self.showNormal()
        else:
            # on osx we can raise the window. on unity the icon in the tray will just flash.
            self.activateWindow()
            self.raise_()
        if buf == "raise":
            return
        # import
        self.handleImport(buf)
Example #31
0
 def onBuryCard(self):
     self.mw.checkpoint(_("Bury"))
     self.mw.col.sched.buryCards([self.card.id])
     self.mw.reset()
     tooltip(_("Card buried."))
Example #32
0
def checkInvalidFilename(str, dirsep=True):
    bad = invalidFilename(str, dirsep)
    if bad:
        showWarning(_("The following character can not be used: %s") % bad)
        return True
    return False
Example #33
0
def openLink(link):
    tooltip(_("Loading..."), period=1000)
    with noBundledLibs():
        QDesktopServices.openUrl(QUrl(link))
Example #34
0
    """
    field_name = None
    for c, name in enumerate(mw.col.models.fieldNames(n.model())):
        for f in id_fields:
            if f == name.lower():
                field_name = name
                field_index = c
                # I would like to break out of the nested for loops
                # here. In C++ you are allowed to use a goto for that.
                # ^_^ Nothing bad will happen when we go on, though.
    if not field_name:
        return flag
    # Field already filled
    if n[field_name]:
        return flag
    # event not coming from id field?
    if field_index != fidx:
        return flag
    # Got to here: We have an empty id field, so put in a number.
    n[field_name] = str(n.id - long(13e11))
    return True


if show_menu_item:
    add_nid = QAction(mw)
    mw.form.menuTools.addAction(add_nid)
    add_nid.setText(_(u"Add note ids"))
    mw.connect(add_nid, SIGNAL("triggered()"), add_nids_to_all)

addHook('editFocusLost', onFocusLost)
Example #35
0
    def fixIntegrity(self):
        "Fix possible problems and rebuild caches."
        problems = []
        self.save()
        oldSize = os.stat(self.path)[stat.ST_SIZE]
        if self.db.scalar("pragma integrity_check") != "ok":
            return (_("Collection is corrupt. Please see the manual."), False)
        # note types with a missing model
        ids = self.db.list("""
select id from notes where mid not in """ + ids2str(self.models.ids()))
        if ids:
            problems.append(
                ngettext("Deleted %d note with missing note type.",
                         "Deleted %d notes with missing note type.", len(ids))
                % len(ids))
            self.remNotes(ids)
        # for each model
        for m in self.models.all():
            for t in m['tmpls']:
                if t['did'] == "None":
                    t['did'] = None
                    problems.append(_("Fixed AnkiDroid deck override bug."))
                    self.models.save(m)
            if m['type'] == MODEL_STD:
                # model with missing req specification
                if 'req' not in m:
                    self.models._updateRequired(m)
                    problems.append(_("Fixed note type: %s") % m['name'])
                # cards with invalid ordinal
                ids = self.db.list(
                    """
select id from cards where ord not in %s and nid in (
select id from notes where mid = ?)""" %
                    ids2str([t['ord'] for t in m['tmpls']]), m['id'])
                if ids:
                    problems.append(
                        ngettext("Deleted %d card with missing template.",
                                 "Deleted %d cards with missing template.",
                                 len(ids)) % len(ids))
                    self.remCards(ids)
            # notes with invalid field count
            ids = []
            for id, flds in self.db.execute(
                    "select id, flds from notes where mid = ?", m['id']):
                if (flds.count("\x1f") + 1) != len(m['flds']):
                    ids.append(id)
            if ids:
                problems.append(
                    ngettext("Deleted %d note with wrong field count.",
                             "Deleted %d notes with wrong field count.",
                             len(ids)) % len(ids))
                self.remNotes(ids)
        # delete any notes with missing cards
        ids = self.db.list("""
select id from notes where id not in (select distinct nid from cards)""")
        if ids:
            cnt = len(ids)
            problems.append(
                ngettext("Deleted %d note with no cards.",
                         "Deleted %d notes with no cards.", cnt) % cnt)
            self._remNotes(ids)
        # cards with missing notes
        ids = self.db.list("""
select id from cards where nid not in (select id from notes)""")
        if ids:
            cnt = len(ids)
            problems.append(
                ngettext("Deleted %d card with missing note.",
                         "Deleted %d cards with missing note.", cnt) % cnt)
            self.remCards(ids)
        # cards with odue set when it shouldn't be
        ids = self.db.list("""
select id from cards where odue > 0 and (type=1 or queue=2) and not odid""")
        if ids:
            cnt = len(ids)
            problems.append(
                ngettext("Fixed %d card with invalid properties.",
                         "Fixed %d cards with invalid properties.", cnt) % cnt)
            self.db.execute("update cards set odue=0 where id in " +
                            ids2str(ids))
        # cards with odid set when not in a dyn deck
        dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)]
        ids = self.db.list("""
select id from cards where odid > 0 and did in %s""" % ids2str(dids))
        if ids:
            cnt = len(ids)
            problems.append(
                ngettext("Fixed %d card with invalid properties.",
                         "Fixed %d cards with invalid properties.", cnt) % cnt)
            self.db.execute("update cards set odid=0, odue=0 where id in " +
                            ids2str(ids))
        # tags
        self.tags.registerNotes()
        # field cache
        for m in self.models.all():
            self.updateFieldCache(self.models.nids(m))
        # new cards can't have a due position > 32 bits
        self.db.execute(
            """
update cards set due = 1000000, mod = ?, usn = ? where due > 1000000
and type = 0""", intTime(), self.usn())
        # new card position
        self.conf['nextPos'] = self.db.scalar(
            "select max(due)+1 from cards where type = 0") or 0
        # reviews should have a reasonable due #
        ids = self.db.list(
            "select id from cards where queue = 2 and due > 100000")
        if ids:
            problems.append("Reviews had incorrect due date.")
            self.db.execute(
                "update cards set due = ?, ivl = 1, mod = ?, usn = ? where id in %s"
                % ids2str(ids), self.sched.today, intTime(), self.usn())
        # v2 sched had a bug that could create decimal intervals
        curs = self.db.cursor()

        curs.execute(
            "update cards set ivl=round(ivl),due=round(due) where ivl!=round(ivl) or due!=round(due)"
        )
        if curs.rowcount:
            problems.append("Fixed %d cards with v2 scheduler bug." %
                            curs.rowcount)

        curs.execute(
            "update revlog set ivl=round(ivl),lastIvl=round(lastIvl) where ivl!=round(ivl) or lastIvl!=round(lastIvl)"
        )
        if curs.rowcount:
            problems.append(
                "Fixed %d review history entries with v2 scheduler bug." %
                curs.rowcount)
        # and finally, optimize
        self.optimize()
        newSize = os.stat(self.path)[stat.ST_SIZE]
        txt = _("Database rebuilt and optimized.")
        ok = not problems
        problems.append(txt)
        # if any problems were found, force a full sync
        if not ok:
            self.modSchema(check=False)
        self.save()
        return ("\n".join(problems), ok)
Example #36
0
 def onReplayRecorded(self):
     if not self._recordedAudio:
         return tooltip(_("You haven't recorded your voice yet."))
     clearAudioQueue()
     play(self._recordedAudio)
Example #37
0
 def onBuryNote(self):
     self.mw.checkpoint(_("Bury"))
     self.mw.col.sched.buryNote(self.card.nid)
     self.mw.reset()
     tooltip(_("Note buried."))
Example #38
0
 def onSuspend(self):
     self.mw.checkpoint(_("Suspend"))
     self.mw.col.sched.suspendCards(
         [c.id for c in self.card.note().cards()])
     tooltip(_("Note suspended."))
     self.mw.reset()
Example #39
0
 def onLeech(self, card):
     # for now
     s = _("Card was a leech.")
     if card.queue < 0:
         s += " " + _("It has been suspended.")
     tooltip(s)
Example #40
0
 def onSuspendCard(self):
     self.mw.checkpoint(_("Suspend"))
     self.mw.col.sched.suspendCards([self.card.id])
     tooltip(_("Card suspended."))
     self.mw.reset()
Example #41
0
    def onOdueInvalid(self):
        showWarning(
            _("""\
Invalid property found on card. Please use Tools>Check Database, \
and if the problem comes up again, please ask on the support site."""))
Example #42
0
 def _contextMenu(self):
     opts = [
         [
             _("Flag Card"),
             [
                 [_("Red Flag"), "Ctrl+1", lambda: self.setFlag(1)],
                 [_("Purple Flag"), "Ctrl+2", lambda: self.setFlag(2)],
                 [_("Green Flag"), "Ctrl+3", lambda: self.setFlag(3)],
                 [_("Blue Flag"), "Ctrl+4", lambda: self.setFlag(4)],
                 None,
                 [_("Clear Flag"), "Ctrl+0", lambda: self.setFlag(0)],
             ]
         ],
         [_("Mark Note"), "*", self.onMark],
         [_("Bury Card"), "-", self.onBuryCard],
         [_("Bury Note"), "=", self.onBuryNote],
         [_("Suspend Card"), "@", self.onSuspendCard],
         [_("Suspend Note"), "!", self.onSuspend],
         [_("Delete Note"), "Ctrl+Delete", self.onDelete],
         [_("Options"), "O", self.onOptions],
         None,
         [_("Replay Audio"), "R", self.replayAudio],
         [_("Record Own Voice"), "Shift+V", self.onRecordVoice],
         [_("Replay Own Voice"), "V", self.onReplayRecorded],
     ]
     return opts
Example #43
0
    def cardGraph(self):
        # graph data
        div = self._cards()
        d = []
        for c, (t, col) in enumerate(
            ((_("Mature"), colMature), (_("Young+Learn"), colYoung),
             (_("Unseen"), colUnseen), (_("Suspended+Buried"), colSusp))):
            d.append(dict(data=div[c], label="%s: %s" % (t, div[c]),
                          color=col))
        # text data
        i = []
        (c, f) = self.col.db.first("""
select count(id), count(distinct nid) from cards
where did in %s """ % self._limit())
        self._line(i, _("Total cards"), c)
        self._line(i, _("Total notes"), f)
        (low, avg, high) = self._factors()
        if low:
            self._line(i, _("Lowest ease"), "%d%%" % low)
            self._line(i, _("Average ease"), "%d%%" % avg)
            self._line(i, _("Highest ease"), "%d%%" % high)
        info = "<table width=100%>" + "".join(i) + "</table><p>"
        info += _('''\
A card's <i>ease</i> is the size of the next interval \
when you answer "good" on a review.''')
        txt = self._title(_("Card Types"),
                          _("The division of cards in your deck(s)."))
        txt += "<table width=%d><tr><td>%s</td><td>%s</td></table>" % (
            self.width, self._graph(id="cards", data=d, type="pie"), info)
        return txt
Example #44
0
 def _selectedDeck(self) -> Optional[Dict[str, Any]]:
     did = self.col.decks.selected()
     if not self.col.decks.nameOrNone(did):
         showInfo(_("Please select a deck."))
         return None
     return self.col.decks.get(did)
Example #45
0
 def _ansInfo(self,
              totd,
              studied,
              first,
              unit,
              convHours=False,
              total=None):
     if not totd:
         return
     tot = totd[-1][1]
     period = self._periodDays()
     if not period:
         # base off earliest repetition date
         period = self._deckAge('review')
     i = []
     self._line(
         i,
         _("Days studied"),
         _("<b>%(pct)d%%</b> (%(x)s of %(y)s)") %
         dict(x=studied, y=period, pct=studied / float(period) * 100),
         bold=False)
     if convHours:
         tunit = _("hours")
     else:
         tunit = unit
     self._line(i, _("Total"),
                _("%(tot)s %(unit)s") % dict(unit=tunit, tot=int(tot)))
     if convHours:
         # convert to minutes
         tot *= 60
     self._line(i, _("Average for days studied"),
                self._avgDay(tot, studied, unit))
     if studied != period:
         # don't display if you did study every day
         self._line(i, _("If you studied every day"),
                    self._avgDay(tot, period, unit))
     if total and tot:
         perMin = total / float(tot)
         perMin = round(perMin, 1)
         # don't round down to zero
         if perMin < 0.1:
             text = _("less than 0.1 cards/minute")
         else:
             text = _("%.01f cards/minute") % perMin
         self._line(
             i, _("Average answer time"),
             _("%(a)0.1fs (%(b)s)") % dict(a=(tot * 60) / total, b=text))
     return self._lineTbl(i), int(tot)
Example #46
0
    def _graph(self,
               id,
               data,
               conf=None,
               type="bars",
               xunit=1,
               ylabel=_("Cards"),
               ylabel2=""):
        if conf is None:
            conf = {}
        # display settings
        if type == "pie":
            conf['legend'] = {'container': "#%sLegend" % id, 'noColumns': 2}
        else:
            conf['legend'] = {'container': "#%sLegend" % id, 'noColumns': 10}
        conf['series'] = dict(stack=True)
        if not 'yaxis' in conf:
            conf['yaxis'] = {}
        conf['yaxis']['labelWidth'] = 40
        if 'xaxis' not in conf:
            conf['xaxis'] = {}
        if xunit is None:
            conf['timeTicks'] = False
        else:
            conf['timeTicks'] = {1: _("d"), 7: _("w"), 31: _("mo")}[xunit]
        # types
        width = self.width
        height = self.height
        if type == "bars":
            conf['series']['bars'] = dict(show=True,
                                          barWidth=0.8,
                                          align="center",
                                          fill=0.7,
                                          lineWidth=0)
        elif type == "barsLine":
            print("deprecated - use 'bars' instead")
            conf['series']['bars'] = dict(show=True,
                                          barWidth=0.8,
                                          align="center",
                                          fill=0.7,
                                          lineWidth=3)
        elif type == "fill":
            conf['series']['lines'] = dict(show=True, fill=True)
        elif type == "pie":
            width /= 2.3
            height *= 1.5
            ylabel = ""
            conf['series']['pie'] = dict(show=True,
                                         radius=1,
                                         stroke=dict(color="#fff", width=5),
                                         label=dict(show=True,
                                                    radius=0.8,
                                                    threshold=0.01,
                                                    background=dict(
                                                        opacity=0.5,
                                                        color="#000")))
        return ("""
<table cellpadding=0 cellspacing=10>
<tr>

<td><div style="width: 150px; text-align: center; position:absolute;
 -webkit-transform: rotate(-90deg) translateY(-85px);
font-weight: bold;
">%(ylab)s</div></td>

<td>
<center><div id=%(id)sLegend></div></center>
<div id="%(id)s" style="width:%(w)spx; height:%(h)spx;"></div>
</td>

<td><div style="width: 150px; text-align: center; position:absolute;
 -webkit-transform: rotate(90deg) translateY(65px);
font-weight: bold;
">%(ylab2)s</div></td>

</tr></table>
<script>
$(function () {
    var conf = %(conf)s;
    if (conf.timeTicks) {
        conf.xaxis.tickFormatter = function (val, axis) {
            return val.toFixed(0)+conf.timeTicks;
        }
    }
    conf.yaxis.minTickSize = 1;
    // prevent ticks from having decimals (use whole numbers instead)
    conf.yaxis.tickDecimals = 0;
    conf.yaxis.tickFormatter = function (val, axis) {
            // Just in case we get ticks with decimals, render to one decimal position.  If it's
            // a whole number then render without any decimal (i.e. without the trailing .0).
            return val === Math.round(val) ? val.toFixed(0) : val.toFixed(1);
    }
    if (conf.series.pie) {
        conf.series.pie.label.formatter = function(label, series){
            return '<div class=pielabel>'+Math.round(series.percent)+'%%</div>';
        };
    }
    $.plot($("#%(id)s"), %(data)s, conf);
});
</script>""" % dict(id=id,
                    w=width,
                    h=height,
                    ylab=ylabel,
                    ylab2=ylabel2,
                    data=json.dumps(data),
                    conf=json.dumps(conf)))
Example #47
0
 def report(self):
     c = self.card
     # pylint: disable=unnecessary-lambda
     fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs)
     self.txt = "<table width=100%>"
     self.addLine(_("Added"), self.date(c.id / 1000))
     first = self.col.db.scalar("select min(id) from revlog where cid = ?",
                                c.id)
     last = self.col.db.scalar("select max(id) from revlog where cid = ?",
                               c.id)
     if first:
         self.addLine(_("First Review"), self.date(first / 1000))
         self.addLine(_("Latest Review"), self.date(last / 1000))
     if c.type in (1, 2):
         if c.odid or c.queue < 0:
             next = None
         else:
             if c.queue in (2, 3):
                 next = time.time() + (
                     (c.due - self.col.sched.today) * 86400)
             else:
                 next = c.due
             next = self.date(next)
         if next:
             self.addLine(_("Due"), next)
         if c.queue == 2:
             self.addLine(_("Interval"), fmt(c.ivl * 86400))
         self.addLine(_("Ease"), "%d%%" % (c.factor / 10.0))
         self.addLine(_("Reviews"), "%d" % c.reps)
         self.addLine(_("Lapses"), "%d" % c.lapses)
         (cnt, total) = self.col.db.first(
             "select count(), sum(time)/1000 from revlog where cid = :id",
             id=c.id)
         if cnt:
             self.addLine(_("Average Time"), self.time(total / float(cnt)))
             self.addLine(_("Total Time"), self.time(total))
     elif c.queue == 0:
         self.addLine(_("Position"), c.due)
     self.addLine(_("Card Type"), c.template()['name'])
     self.addLine(_("Note Type"), c.model()['name'])
     self.addLine(_("Deck"), self.col.decks.name(c.did))
     self.addLine(_("Note ID"), c.nid)
     self.addLine(_("Card ID"), c.id)
     self.txt += "</table>"
     return self.txt
Example #48
0
 def hourGraph(self):
     data = self._hourRet()
     if not data:
         return ""
     shifted = []
     counts = []
     mcount = 0
     trend = []
     peak = 0
     for d in data:
         hour = (d[0] - 4) % 24
         pct = d[1]
         if pct > peak:
             peak = pct
         shifted.append((hour, pct))
         counts.append((hour, d[2]))
         if d[2] > mcount:
             mcount = d[2]
     shifted.sort()
     counts.sort()
     if len(counts) < 4:
         return ""
     for d in shifted:
         hour = d[0]
         pct = d[1]
         if not trend:
             trend.append((hour, pct))
         else:
             prev = trend[-1][1]
             diff = pct - prev
             diff /= 3.0
             diff = round(diff, 1)
             trend.append((hour, prev + diff))
     txt = self._title(_("Hourly Breakdown"),
                       _("Review success rate for each hour of the day."))
     txt += self._graph(
         id="hour",
         data=[
             dict(data=shifted, color=colCum, label=_("% Correct")),
             dict(data=counts,
                  color=colHour,
                  label=_("Answers"),
                  yaxis=2,
                  bars=dict(barWidth=0.2),
                  stack=False)
         ],
         conf=dict(xaxis=dict(ticks=[[0, _("4AM")], [6, _(
             "10AM")], [12, _("4PM")], [18, _("10PM")], [23, _("3AM")]]),
                   yaxes=[
                       dict(max=peak),
                       dict(position="right", max=mcount)
                   ]),
         ylabel=_("% Correct"),
         ylabel2=_("Reviews"))
     txt += _("Hours with less than 30 reviews are not shown.")
     return txt
Example #49
0
class AnkiPackageExporter(AnkiExporter):

    key = _("Anki Deck Package")
    ext = ".apkg"

    def __init__(self, col: _Collection) -> None:
        AnkiExporter.__init__(self, col)

    def exportInto(self, path: str) -> None:
        # open a zip file
        z = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED, allowZip64=True)
        media = self.doExport(z, path)
        # media map
        z.writestr("media", json.dumps(media))
        z.close()

    def doExport(self, z: ZipFile,
                 path: str) -> Dict[str, str]:  # type: ignore
        # export into the anki2 file
        colfile = path.replace(".apkg", ".anki2")
        AnkiExporter.exportInto(self, colfile)
        if not self._v2sched:
            z.write(colfile, "collection.anki2")
        else:
            # prevent older clients from accessing
            # pylint: disable=unreachable
            self._addDummyCollection(z)
            z.write(colfile, "collection.anki21")

        # and media
        self.prepareMedia()
        media = self._exportMedia(z, self.mediaFiles, self.mediaDir)
        # tidy up intermediate files
        os.unlink(colfile)
        p = path.replace(".apkg", ".media.db2")
        if os.path.exists(p):
            os.unlink(p)
        os.chdir(self.mediaDir)
        shutil.rmtree(path.replace(".apkg", ".media"))
        return media

    def _exportMedia(self, z: ZipFile, files: List[str],
                     fdir: str) -> Dict[str, str]:
        media = {}
        for c, file in enumerate(files):
            cStr = str(c)
            mpath = os.path.join(fdir, file)
            if os.path.isdir(mpath):
                continue
            if os.path.exists(mpath):
                if re.search(r'\.svg$', file, re.IGNORECASE):
                    z.write(mpath, cStr, zipfile.ZIP_DEFLATED)
                else:
                    z.write(mpath, cStr, zipfile.ZIP_STORED)
                media[cStr] = unicodedata.normalize("NFC", file)
                runHook("exportedMediaFiles", c)

        return media

    def prepareMedia(self) -> None:
        # chance to move each file in self.mediaFiles into place before media
        # is zipped up
        pass

    # create a dummy collection to ensure older clients don't try to read
    # data they don't understand
    def _addDummyCollection(self, zip) -> None:
        path = namedtmp("dummy.anki2")
        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.anki2")
        os.unlink(path)
Example #50
0
    def repsGraphs(self):
        start, days, chunk = self.get_start_end_chunk()
        data = self._done(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)

        # reps
        (repdata, repsum) = self._splitRepData(
            data, ((3, colMature, _("Mature")), (2, colYoung, _("Young")),
                   (4, colRelearn, _("Relearn")), (1, colLearn, _("Learn")),
                   (5, colCram, _("Cram"))))
        txt1 = self._title(_("Review Count"),
                           _("The number of questions you have answered."))
        txt1 += plot("reps",
                     repdata,
                     ylabel=_("Answers"),
                     ylabel2=_("Cumulative Answers"))
        (daysStud, fstDay) = self._daysStudied()
        rep, tot = self._ansInfo(repsum, daysStud, fstDay, _("reviews"))
        txt1 += rep
        # time
        (timdata, timsum) = self._splitRepData(
            data, ((8, colMature, _("Mature")), (7, colYoung, _("Young")),
                   (9, colRelearn, _("Relearn")), (6, colLearn, _("Learn")),
                   (10, colCram, _("Cram"))))
        if self.type == 0:
            t = _("Minutes")
            convHours = False
        else:
            t = _("Hours")
            convHours = True
        txt2 = self._title(_("Review Time"),
                           _("The time taken to answer the questions."))
        txt2 += plot("time", timdata, ylabel=t, ylabel2=_("Cumulative %s") % t)
        rep, tot2 = self._ansInfo(timsum,
                                  daysStud,
                                  fstDay,
                                  _("minutes"),
                                  convHours,
                                  total=tot)
        txt2 += rep
        return self._section(txt1) + self._section(txt2)
Example #51
0
class AnkiExporter(Exporter):

    key = _("Anki 2.0 Deck")
    ext = ".anki2"

    def __init__(self, col):
        Exporter.__init__(self, col)
        self.includeSched = False
        self.includeMedia = True

    def exportInto(self, path):
        # create a new collection at the target
        try:
            os.unlink(path)
        except (IOError, OSError):
            pass
        self.dst = Collection(path)
        self.src = self.col
        # find cards
        if not self.did:
            cids = self.src.db.list("select id from cards")
        else:
            cids = self.src.decks.cids(self.did, children=True)
        # copy cards, noting used nids
        nids = {}
        data = []
        for row in self.src.db.execute(
            "select * from cards where id in "+ids2str(cids)):
            nids[row[1]] = True
            data.append(row)
        self.dst.db.executemany(
            "insert into cards values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
            data)
        # notes
        strnids = ids2str(nids.keys())
        notedata = self.src.db.all("select * from notes where id in "+
                               strnids)
        self.dst.db.executemany(
            "insert into notes values (?,?,?,?,?,?,?,?,?,?,?)",
            notedata)
        # models used by the notes
        mids = self.dst.db.list("select distinct mid from notes where id in "+
                                strnids)
        # card history and revlog
        if self.includeSched:
            data = self.src.db.all(
                "select * from revlog where cid in "+ids2str(cids))
            self.dst.db.executemany(
                "insert into revlog values (?,?,?,?,?,?,?,?,?)",
                data)
        else:
            # need to reset card state
            self.dst.sched.resetCards(cids)
        # models
        for m in self.src.models.all():
            if int(m['id']) in mids:
                self.dst.models.update(m)
        # decks
        if not self.did:
            dids = []
        else:
            dids = [self.did] + [
                x[1] for x in self.src.decks.children(self.did)]
        dconfs = {}
        for d in self.src.decks.all():
            if d['id'] == 1:
                continue
            if dids and d['id'] not in dids:
                continue
            if not d['dyn'] and d['conf'] != 1:
                if self.includeSched:
                    dconfs[d['conf']] = True
            if not self.includeSched:
                # scheduling not included, so reset deck settings to default
                d = dict(d)
                d['conf'] = 1
            self.dst.decks.update(d)
        # copy used deck confs
        for dc in self.src.decks.allConf():
            if dc['id'] in dconfs:
                self.dst.decks.updateConf(dc)
        # find used media
        media = {}
        if self.includeMedia:
            for row in notedata:
                flds = row[6]
                mid = row[2]
                for file in self.src.media.filesInStr(mid, flds):
                    media[file] = True
        self.mediaFiles = media.keys()
        self.mediaDir = self.src.media.dir()
        self.dst.crt = self.src.crt
        # todo: tags?
        self.count = self.dst.cardCount()
        self.dst.setMod()
        self.postExport()
        self.dst.close()

    def postExport(self):
        # overwrite to apply customizations to the deck before it's closed,
        # such as update the deck description
        pass
Example #52
0
    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
Example #53
0
 def finishedMsg(self):
     return ("<b>"+_(
         "Congratulations! You have finished for now.")+
         "</b><br><br>" + self._nextDueMsg())
Example #54
0
class AnkiExporter(Exporter):

    key = _("Anki 2.0 Deck")
    ext = ".anki2"
    includeSched: typing.Union[bool, None] = False
    includeMedia = True

    def __init__(self, col: _Collection) -> None:
        Exporter.__init__(self, col)

    def exportInto(self, path: str) -> None:
        # sched info+v2 scheduler not compatible w/ older clients
        self._v2sched = self.col.schedVer() != 1 and self.includeSched

        # create a new collection at the target
        try:
            os.unlink(path)
        except (IOError, OSError):
            pass
        self.dst = Collection(path)
        self.src = self.col
        # find cards
        cids = self.cardIds()
        # copy cards, noting used nids
        nids = {}
        data = []
        for row in self.src.db.execute("select * from cards where id in " +
                                       ids2str(cids)):
            nids[row[1]] = True
            data.append(row)
            # clear flags
            row = list(row)
            row[-2] = 0
        self.dst.db.executemany(
            "insert into cards values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
            data)
        # notes
        strnids = ids2str(list(nids.keys()))
        notedata = []
        for row in self.src.db.all("select * from notes where id in " +
                                   strnids):
            # remove system tags if not exporting scheduling info
            if not self.includeSched:
                row = list(row)
                row[5] = self.removeSystemTags(row[5])
            notedata.append(row)
        self.dst.db.executemany(
            "insert into notes values (?,?,?,?,?,?,?,?,?,?,?)", notedata)
        # models used by the notes
        mids = self.dst.db.list("select distinct mid from notes where id in " +
                                strnids)
        # card history and revlog
        if self.includeSched:
            data = self.src.db.all("select * from revlog where cid in " +
                                   ids2str(cids))
            self.dst.db.executemany(
                "insert into revlog values (?,?,?,?,?,?,?,?,?)", data)
        else:
            # need to reset card state
            self.dst.sched.resetCards(cids)
        # models - start with zero
        self.dst.models.models = {}
        for m in self.src.models.all():
            if int(m['id']) in mids:
                self.dst.models.update(m)
        # decks
        dids: List[int]
        if not self.did:
            dids = []
        else:
            dids = [self.did
                    ] + [x[1] for x in self.src.decks.children(self.did)]
        dconfs = {}
        for d in self.src.decks.all():
            if str(d['id']) == "1":
                continue
            if dids and d['id'] not in dids:
                continue
            if not d['dyn'] and d['conf'] != 1:
                if self.includeSched:
                    dconfs[d['conf']] = True
            if not self.includeSched:
                # scheduling not included, so reset deck settings to default
                d = dict(d)
                d['conf'] = 1
            self.dst.decks.update(d)
        # copy used deck confs
        for dc in self.src.decks.allConf():
            if dc['id'] in dconfs:
                self.dst.decks.updateConf(dc)
        # find used media
        media = {}
        self.mediaDir = self.src.media.dir()
        if self.includeMedia:
            for row in notedata:
                flds = row[6]
                mid = row[2]
                for file in self.src.media.filesInStr(mid, flds):
                    # skip files in subdirs
                    if file != os.path.basename(file):
                        continue
                    media[file] = True
            if self.mediaDir:
                for fname in os.listdir(self.mediaDir):
                    path = os.path.join(self.mediaDir, fname)
                    if os.path.isdir(path):
                        continue
                    if fname.startswith("_"):
                        # Scan all models in mids for reference to fname
                        for m in self.src.models.all():
                            if int(m['id']) in mids:
                                if self._modelHasMedia(m, fname):
                                    media[fname] = True
                                    break
        self.mediaFiles = list(media.keys())
        self.dst.crt = self.src.crt
        # todo: tags?
        self.count = self.dst.cardCount()
        self.dst.setMod()
        self.postExport()
        self.dst.close()

    def postExport(self) -> None:
        # overwrite to apply customizations to the deck before it's closed,
        # such as update the deck description
        pass

    def removeSystemTags(self, tags: str) -> Any:
        return self.src.tags.remFromStr("marked leech", tags)

    def _modelHasMedia(self, model, fname) -> bool:
        # First check the styling
        if fname in model["css"]:
            return True
        # If no reference to fname then check the templates as well
        for t in model["tmpls"]:
            if fname in t["qfmt"] or fname in t["afmt"]:
                return True
        return False
Example #55
0
    def _repsGraph(self, data, days, reptitle, timetitle):
        if not data:
            return ""
        d = data
        conf = dict(xaxis=dict(tickDecimals=0, max=0.5),
                    yaxes=[dict(), dict(position="right")])
        if days is not None:
            conf['xaxis']['min'] = -days + 0.5

        def plot(id, data, ylabel, ylabel2):
            return self._graph(id,
                               data=data,
                               conf=conf,
                               ylabel=ylabel,
                               ylabel2=ylabel2)

        # reps
        (repdata, repsum) = self._splitRepData(
            d, ((3, colMature, _("Mature")), (2, colYoung, _("Young")),
                (4, colRelearn, _("Relearn")), (1, colLearn, _("Learn")),
                (5, colCram, _("Cram"))))
        txt = self._title(reptitle,
                          _("The number of questions you have answered."))
        txt += plot("reps",
                    repdata,
                    ylabel=_("Answers"),
                    ylabel2=_("Cumulative Answers"))
        (daysStud, fstDay) = self._daysStudied()
        rep, tot = self._ansInfo(repsum, daysStud, fstDay, _("reviews"))
        txt += rep
        # time
        (timdata, timsum) = self._splitRepData(
            d, ((8, colMature, _("Mature")), (7, colYoung, _("Young")),
                (9, colRelearn, _("Relearn")), (6, colLearn, _("Learn")),
                (10, colCram, _("Cram"))))
        if self.type == 0:
            t = _("Minutes")
            convHours = False
        else:
            t = _("Hours")
            convHours = True
        txt += self._title(timetitle,
                           _("The time taken to answer the questions."))
        txt += plot("time", timdata, ylabel=t, ylabel2=_("Cumulative %s") % t)
        rep, tot2 = self._ansInfo(timsum,
                                  daysStud,
                                  fstDay,
                                  _("minutes"),
                                  convHours,
                                  total=tot)
        txt += rep
        return txt
Example #56
0
    def accept(self):
        self.exporter.includeSched = (self.frm.includeSched.isChecked())
        self.exporter.includeMedia = (self.frm.includeMedia.isChecked())
        self.exporter.includeTags = (self.frm.includeTags.isChecked())
        self.exporter.includeHTML = (self.frm.includeHTML.isChecked())
        if not self.frm.deck.currentIndex():
            self.exporter.did = None
        else:
            name = self.decks[self.frm.deck.currentIndex()]
            self.exporter.did = self.col.decks.id(name)
        if self.isVerbatim:
            name = time.strftime("-%Y-%m-%d@%H-%M-%S",
                                 time.localtime(time.time()))
            deck_name = _("collection") + name
        else:
            # Get deck name and remove invalid filename characters
            deck_name = self.decks[self.frm.deck.currentIndex()]
            deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name)

        if not self.isVerbatim and self.isApkg and self.exporter.includeSched and self.col.schedVer(
        ) == 2:
            showInfo(
                "Please switch to the regular scheduler before exporting a single deck .apkg with scheduling."
            )
            return

        filename = '{0}{1}'.format(deck_name, self.exporter.ext)
        while 1:
            file = getSaveFile(self,
                               _("Export"),
                               "export",
                               self.exporter.key,
                               self.exporter.ext,
                               fname=filename)
            if not file:
                return
            if checkInvalidFilename(os.path.basename(file), dirsep=False):
                continue
            break
        self.hide()
        if file:
            self.mw.progress.start(immediate=True)
            try:
                f = open(file, "wb")
                f.close()
            except (OSError, IOError) as e:
                showWarning(_("Couldn't save file: %s") % str(e))
            else:
                os.unlink(file)
                exportedMedia = lambda cnt: self.mw.progress.update(
                    label=ngettext("Exported %d media file",
                                   "Exported %d media files", cnt) % cnt)
                addHook("exportedMediaFiles", exportedMedia)
                self.exporter.exportInto(file)
                remHook("exportedMediaFiles", exportedMedia)
                period = 3000
                if self.isVerbatim:
                    msg = _("Collection exported.")
                else:
                    if self.isTextNote:
                        msg = ngettext(
                            "%d note exported.", "%d notes exported.",
                            self.exporter.count) % self.exporter.count
                    else:
                        msg = ngettext(
                            "%d card exported.", "%d cards exported.",
                            self.exporter.count) % self.exporter.count
                tooltip(msg, period=period)
            finally:
                self.mw.progress.finish()
        QDialog.accept(self)
Example #57
0
 def _contextMenu(self):
     currentFlag = self.card and self.card.userFlag()
     opts = [
         [_("Flag Card"), [
             [_("Red Flag"), "Ctrl+1", lambda: self.setFlag(1),
              dict(checked=currentFlag == 1)],
             [_("Orange Flag"), "Ctrl+2", lambda: self.setFlag(2),
              dict(checked=currentFlag == 2)],
             [_("Green Flag"), "Ctrl+3", lambda: self.setFlag(3),
              dict(checked=currentFlag == 3)],
             [_("Blue Flag"), "Ctrl+4", lambda: self.setFlag(4),
              dict(checked=currentFlag == 4)],
         ]],
         [_("Mark Note"), "*", self.onMark],
         [_("Bury Card"), "-", self.onBuryCard],
         [_("Bury Note"), "=", self.onBuryNote],
         [_("Suspend Card"), "@", self.onSuspendCard],
         [_("Suspend Note"), "!", self.onSuspend],
         [_("Delete Note"), "Ctrl+Delete", self.onDelete],
         [_("Options"), "O", self.onOptions],
         None,
         [_("Replay Audio"), "R", self.replayAudio],
         [_("Record Own Voice"), "Shift+V", self.onRecordVoice],
         [_("Replay Own Voice"), "V", self.onReplayRecorded],
     ]
     return opts
Example #58
0
    def _graph(self,
               id,
               data,
               conf={},
               type="bars",
               ylabel=_("Cards"),
               timeTicks=True,
               ylabel2=""):
        # display settings
        if type == "pie":
            conf['legend'] = {'container': "#%sLegend" % id, 'noColumns': 2}
        else:
            conf['legend'] = {'container': "#%sLegend" % id, 'noColumns': 10}
        conf['series'] = dict(stack=True)
        if not 'yaxis' in conf:
            conf['yaxis'] = {}
        conf['yaxis']['labelWidth'] = 40
        if 'xaxis' not in conf:
            conf['xaxis'] = {}
        if timeTicks:
            conf['timeTicks'] = (_("d"), _("w"), _("m"))[self.type]
        # types
        width = self.width
        height = self.height
        if type == "bars":
            conf['series']['bars'] = dict(show=True,
                                          barWidth=0.8,
                                          align="center",
                                          fill=0.7,
                                          lineWidth=0)
        elif type == "barsLine":
            conf['series']['bars'] = dict(show=True,
                                          barWidth=0.8,
                                          align="center",
                                          fill=0.7,
                                          lineWidth=3)
        elif type == "fill":
            conf['series']['lines'] = dict(show=True, fill=True)
        elif type == "pie":
            width /= 2.3
            height *= 1.5
            ylabel = ""
            conf['series']['pie'] = dict(show=True,
                                         radius=1,
                                         stroke=dict(color="#fff", width=5),
                                         label=dict(show=True,
                                                    radius=0.8,
                                                    threshold=0.01,
                                                    background=dict(
                                                        opacity=0.5,
                                                        color="#000")))

            #conf['legend'] = dict(show=False)
        return ("""
<table cellpadding=0 cellspacing=10>
<tr>

<td><div style="width: 150px; text-align: center; position:absolute;
 -webkit-transform: rotate(-90deg) translateY(-85px);
font-weight: bold;
">%(ylab)s</div></td>

<td>
<center><div id=%(id)sLegend></div></center>
<div id="%(id)s" style="width:%(w)s; height:%(h)s;"></div>
</td>

<td><div style="width: 150px; text-align: center; position:absolute;
 -webkit-transform: rotate(90deg) translateY(65px);
font-weight: bold;
">%(ylab2)s</div></td>

</tr></table>
<script>
$(function () {
    var conf = %(conf)s;
    if (conf.timeTicks) {
        conf.xaxis.tickFormatter = function (val, axis) {
            return val.toFixed(0)+conf.timeTicks;
        }
    }
    conf.yaxis.minTickSize = 1;
    conf.yaxis.tickFormatter = function (val, axis) {
            return val.toFixed(0);
    }
    if (conf.series.pie) {
        conf.series.pie.label.formatter = function(label, series){
            return '<div class=pielabel>'+Math.round(series.percent)+'%%</div>';
        };
    }
    $.plot($("#%(id)s"), %(data)s, conf);
});
</script>""" % dict(id=id,
                    w=width,
                    h=height,
                    ylab=ylabel,
                    ylab2=ylabel2,
                    data=json.dumps(data),
                    conf=json.dumps(conf)))
Example #59
0
 def retranslateUi(self, Dialog):
     _translate = QtCore.QCoreApplication.translate
     Dialog.setWindowTitle(_("HTML Editor"))
Example #60
0
 def _dueInfo(self, tot, num):
     i = []
     self._line(i, _("Total"), _("%d reviews") % tot)
     self._line(i, _("Average"), self._avgDay(tot, num, _("reviews")))
     return self._lineTbl(i)