def _dueInfo(self, tot, num): i = [] self._line(i, _("Total"), ngettext("%d review", "%d reviews", tot) % tot) self._line(i, _("Average"), self._avgDay( tot, num, _("reviews"))) tomorrow = self.col.db.scalar(""" select count() from cards where did in %s and queue in (2,3) and due = ?""" % self._limit(), self.col.sched.today+1) tomorrow = ngettext("%d card", "%d cards", tomorrow) % tomorrow self._line(i, _("Due tomorrow"), tomorrow) return self._lineTbl(i)
def introductionGraph(self): start, days, chunk = self.get_start_end_chunk() data = self._added(days, chunk) if not data: return "" conf = dict( xaxis=dict(tickDecimals=0, max=0.5), yaxes=[dict(min=0), dict(position="right", min=0)]) if days is not None: # pylint: disable=invalid-unary-operand-type conf['xaxis']['min'] = -days+0.5 def plot(id, data, ylabel, ylabel2): return self._graph( id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2) # graph repdata, repsum = self._splitRepData(data, ((1, colLearn, ""),)) txt = self._title( _("Added"), _("The number of new cards you have added.")) txt += plot("intro", repdata, ylabel=_("Cards"), ylabel2=_("Cumulative Cards")) # total and per day average tot = sum([i[1] for i in data]) period = self._periodDays() if not period: # base off date of earliest added card period = self._deckAge('add') i = [] self._line(i, _("Total"), ngettext("%d card", "%d cards", tot) % tot) self._line(i, _("Average"), self._avgDay(tot, period, _("cards"))) txt += self._lineTbl(i) return txt
def 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 foreignNotes(self): # Load file and parse it by minidom self.loadSource(self.file) # Migrating content / time consuming part # addItemToCards is called for each sm element self.logger('Parsing started.') self.parse() self.logger('Parsing done.') # Return imported cards self.total = len(self.notes) self.log.append( ngettext("%d card imported.", "%d cards imported.", self.total) % self.total) return self.notes
import traceback from contextlib import contextmanager from anki_lib.lang import _, ngettext # some add-ons expect json to be in the utils module import json # pylint: disable=unused-import # Time handling ############################################################################## def intTime(scale=1): "The time in integer seconds. Pass scale=1000 to get milliseconds." return int(time.time()*scale) timeTable = { "years": lambda n: ngettext("%s year", "%s years", n), "months": lambda n: ngettext("%s month", "%s months", n), "days": lambda n: ngettext("%s day", "%s days", n), "hours": lambda n: ngettext("%s hour", "%s hours", n), "minutes": lambda n: ngettext("%s minute", "%s minutes", n), "seconds": lambda n: ngettext("%s second", "%s seconds", n), } inTimeTable = { "years": lambda n: ngettext("in %s year", "in %s years", n), "months": lambda n: ngettext("in %s month", "in %s months", n), "days": lambda n: ngettext("in %s day", "in %s days", n), "hours": lambda n: ngettext("in %s hour", "in %s hours", n), "minutes": lambda n: ngettext("in %s minute", "in %s minutes", n), "seconds": lambda n: ngettext("in %s second", "in %s seconds", n), }
def run(self): db = DB(self.file) ver = db.scalar( "select value from global_variables where key='version'") if not ver.startswith('Mnemosyne SQL 1') and ver not in ("2", "3"): self.log.append(_("File version unknown, trying import anyway.")) # gather facts into temp objects curid = None notes = {} note = None for _id, id, k, v in db.execute(""" select _id, id, key, value from facts f, data_for_fact d where f._id=d._fact_id"""): if id != curid: if note: # pylint: disable=unsubscriptable-object notes[note['_id']] = note note = {'_id': _id} curid = id note[k] = v if note: notes[note['_id']] = note # gather cards front = [] frontback = [] vocabulary = [] cloze = {} for row in db.execute(""" select _fact_id, fact_view_id, tags, next_rep, last_rep, easiness, acq_reps+ret_reps, lapses, card_type_id from cards"""): # categorize note note = notes[row[0]] if row[1].endswith(".1"): if row[1].startswith("1.") or row[1].startswith("1::"): front.append(note) elif row[1].startswith("2.") or row[1].startswith("2::"): frontback.append(note) elif row[1].startswith("3.") or row[1].startswith("3::"): vocabulary.append(note) elif row[1].startswith("5.1"): cloze[row[0]] = note # check for None to fix issue where import can error out rawTags = row[2] if rawTags is None: rawTags = "" # merge tags into note tags = rawTags.replace(", ", "\x1f").replace(" ", "_") tags = tags.replace("\x1f", " ") if "tags" not in note: note['tags'] = [] note['tags'] += self.col.tags.split(tags) note['tags'] = self.col.tags.canonify(note['tags']) # if it's a new card we can go with the defaults if row[3] == -1: continue # add the card c = ForeignCard() c.factor = int(row[5] * 1000) c.reps = row[6] c.lapses = row[7] # ivl is inferred in mnemosyne next, prev = row[3:5] c.ivl = max(1, (next - prev) / 86400) # work out how long we've got left rem = int((next - time.time()) / 86400) c.due = self.col.sched.today + rem # get ord m = re.search(r".(\d+)$", row[1]) ord = int(m.group(1)) - 1 if 'cards' not in note: note['cards'] = {} note['cards'][ord] = c self._addFronts(front) total = self.total self._addFrontBacks(frontback) total += self.total self._addVocabulary(vocabulary) self.total += total self._addCloze(cloze) self.total += total self.log.append( ngettext("%d note imported.", "%d notes imported.", self.total) % self.total)
def importNotes(self, notes): "Convert each card into a note, apply attributes and add to col." assert self.mappingOk() # note whether tags are mapped self._tagsMapped = False for f in self.mapping: if f == "_tags": self._tagsMapped = True # gather checks for duplicate comparison csums = {} for csum, id in self.col.db.execute( "select csum, id from notes where mid = ?", self.model['id']): if csum in csums: csums[csum].append(id) else: csums[csum] = [id] firsts = {} fld0idx = self.mapping.index(self.model['flds'][0]['name']) self._fmap = self.col.models.fieldMap(self.model) self._nextID = timestampID(self.col.db, "notes") # loop through the notes updates = [] updateLog = [] updateLogTxt = _("First field matched: %s") dupeLogTxt = _("Added duplicate with first field: %s") new = [] self._ids = [] self._cards = [] self._emptyNotes = False dupeCount = 0 dupes = [] for n in notes: for c in range(len(n.fields)): if not self.allowHTML: n.fields[c] = html.escape(n.fields[c], quote=False) n.fields[c] = n.fields[c].strip() if not self.allowHTML: n.fields[c] = n.fields[c].replace("\n", "<br>") n.fields[c] = unicodedata.normalize("NFC", n.fields[c]) n.tags = [unicodedata.normalize("NFC", t) for t in n.tags] fld0 = n.fields[fld0idx] csum = fieldChecksum(fld0) # first field must exist if not fld0: self.log.append( _("Empty first field: %s") % " ".join(n.fields)) continue # earlier in import? if fld0 in firsts and self.importMode != 2: # duplicates in source file; log and ignore self.log.append(_("Appeared twice in file: %s") % fld0) continue firsts[fld0] = True # already exists? found = False if csum in csums: # csum is not a guarantee; have to check for id in csums[csum]: flds = self.col.db.scalar( "select flds from notes where id = ?", id) sflds = splitFields(flds) if fld0 == sflds[0]: # duplicate found = True if self.importMode == 0: data = self.updateData(n, id, sflds) if data: updates.append(data) updateLog.append(updateLogTxt % fld0) dupeCount += 1 found = True elif self.importMode == 1: dupeCount += 1 elif self.importMode == 2: # allow duplicates in this case if fld0 not in dupes: # only show message once, no matter how many # duplicates are in the collection already updateLog.append(dupeLogTxt % fld0) dupes.append(fld0) found = False # newly add if not found: data = self.newData(n) if data: new.append(data) # note that we've seen this note once already firsts[fld0] = True self.addNew(new) self.addUpdates(updates) # make sure to update sflds, etc self.col.updateFieldCache(self._ids) # generate cards if self.col.genCards(self._ids): self.log.insert( 0, _("Empty cards found. Please run Tools>Empty Cards.")) # apply scheduling updates self.updateCards() # we randomize or order here, to ensure that siblings # have the same due# did = self.col.decks.selected() conf = self.col.decks.confForDid(did) # in order due? if conf['new']['order'] == NEW_CARDS_RANDOM: self.col.sched.randomizeCards(did) part1 = ngettext("%d note added", "%d notes added", len(new)) % len(new) part2 = ngettext("%d note updated", "%d notes updated", self.updateCount) % self.updateCount if self.importMode == 0: unchanged = dupeCount - self.updateCount elif self.importMode == 1: unchanged = dupeCount else: unchanged = 0 part3 = ngettext("%d note unchanged", "%d notes unchanged", unchanged) % unchanged self.log.append("%s, %s, %s." % (part1, part2, part3)) self.log.extend(updateLog) if self._emptyNotes: self.log.append( _("""\ One or more notes were not imported, because they didn't generate any cards. \ This can happen when you have empty fields or when you have not mapped the \ content in the text file to the correct fields.""")) self.total = len(self._ids)
def fixIntegrity(self): "Fix possible problems and rebuild caches." problems = [] curs = self.db.cursor() 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, so wrap items over # 2 million back to 1 million curs.execute( """ update cards set due=1000000+due%1000000,mod=?,usn=? where due>=1000000 and type=0""", [intTime(), self.usn()]) if curs.rowcount: problems.append( "Found %d new cards with a due number >= 1,000,000 - consider repositioning them in the Browse screen." % curs.rowcount) # 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.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) # models if self.models.ensureNotEmpty(): problems.append("Added missing note type.") # 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)