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
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))
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)
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
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)
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()
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()
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)
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 ▾</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)
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 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)
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())
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")))
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
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))
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"))
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()
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)
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()
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)
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()
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))
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
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()
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)
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 > 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")
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")
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)
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)
def onBuryCard(self): self.mw.checkpoint(_("Bury")) self.mw.col.sched.buryCards([self.card.id]) self.mw.reset() tooltip(_("Card buried."))
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
def openLink(link): tooltip(_("Loading..."), period=1000) with noBundledLibs(): QDesktopServices.openUrl(QUrl(link))
""" 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)
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)
def onReplayRecorded(self): if not self._recordedAudio: return tooltip(_("You haven't recorded your voice yet.")) clearAudioQueue() play(self._recordedAudio)
def onBuryNote(self): self.mw.checkpoint(_("Bury")) self.mw.col.sched.buryNote(self.card.nid) self.mw.reset() tooltip(_("Note buried."))
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()
def onLeech(self, card): # for now s = _("Card was a leech.") if card.queue < 0: s += " " + _("It has been suspended.") tooltip(s)
def onSuspendCard(self): self.mw.checkpoint(_("Suspend")) self.mw.col.sched.suspendCards([self.card.id]) tooltip(_("Card suspended.")) self.mw.reset()
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."""))
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
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
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)
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)
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)))
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
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
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)
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)
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
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 finishedMsg(self): return ("<b>"+_( "Congratulations! You have finished for now.")+ "</b><br><br>" + self._nextDueMsg())
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
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
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)
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
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)))
def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_("HTML Editor"))
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)