def __init__(self, db): self.db = db self.path = db._path self._lastSave = time.time() self.clearUndo() self.load() if not self.crt: d = datetime.datetime.today() d -= datetime.timedelta(hours=4) d = datetime.datetime(d.year, d.month, d.day) d += datetime.timedelta(hours=4) self.crt = int(time.mktime(d.timetuple())) self.modelCache = {} self.undoEnabled = False self.sessionStartReps = 0 self.sessionStartTime = 0 self.lastSessionStart = 0 self._stdSched = Scheduler(self) self.sched = self._stdSched self.media = MediaRegistry(self) # check for improper shutdown self.cleanup()
class _Deck(object): def __init__(self, db): self.db = db self.path = db._path self._lastSave = time.time() self.clearUndo() self.load() if not self.crt: d = datetime.datetime.today() d -= datetime.timedelta(hours=4) d = datetime.datetime(d.year, d.month, d.day) d += datetime.timedelta(hours=4) self.crt = int(time.mktime(d.timetuple())) self.modelCache = {} self.undoEnabled = False self.sessionStartReps = 0 self.sessionStartTime = 0 self.lastSessionStart = 0 self._stdSched = Scheduler(self) self.sched = self._stdSched self.media = MediaRegistry(self) # check for improper shutdown self.cleanup() def name(self): n = os.path.splitext(os.path.basename(self.path))[0] return n # DB-related ########################################################################## def load(self): (self.crt, self.mod, self.scm, self.dty, self.syncName, self.lastSync, self.qconf, self.conf, self.data) = self.db.first(""" select crt, mod, scm, dty, syncName, lastSync, qconf, conf, data from deck""") self.qconf = simplejson.loads(self.qconf) self.conf = simplejson.loads(self.conf) self.data = simplejson.loads(self.data) def flush(self): "Flush state to DB, updating mod time." self.mod = intTime() self.db.execute( """update deck set crt=?, mod=?, scm=?, dty=?, syncName=?, lastSync=?, qconf=?, conf=?, data=?""", self.crt, self.mod, self.scm, self.dty, self.syncName, self.lastSync, simplejson.dumps(self.qconf), simplejson.dumps(self.conf), simplejson.dumps(self.data)) def save(self, name=None): "Flush, commit DB, and take out another write lock." self.flush() self.db.commit() self.lock() self._markOp(name) self._lastSave = time.time() def autosave(self): "Save if 5 minutes has passed since last save." if time.time() - self._lastSave > 300: self.save() def lock(self): self.db.execute("update deck set mod=mod") def close(self, save=True): "Disconnect from DB." self.cleanup() if self.db: if save: self.save() else: self.rollback() self.db.close() self.db = None def reopen(self): "Reconnect to DB (after changing threads, etc). Doesn't reload." import anki.db if not self.db: self.db = anki.db.DB(self.path) def rollback(self): self.db.rollback() self.load() self.lock() def modSchema(self, check=True): "Mark schema modified. Call this first so user can abort if necessary." if not self.schemaChanged(): if check and not runFilter("modSchema", True): raise AnkiError("abortSchemaMod") # next sync will be full self.emptyTrash() self.scm = intTime() def schemaChanged(self): "True if schema changed since last sync, or syncing off." return not self.syncingEnabled() or self.scm > self.lastSync def setDirty(self): self.dty = True def cleanup(self): if self.dty: self.sched.onClose() self.dty = False def rename(self, path): # close our DB connection self.close() # move to new path shutil.copy2(self.path, path) os.unlink(self.path) # record old dir olddir = self.media.dir() # reconnect & move media self.path = path self.reopen() self.media.move(olddir) # Object creation helpers ########################################################################## def getCard(self, id): return anki.cards.Card(self, id) def getFact(self, id): return anki.facts.Fact(self, id=id) def getModel(self, mid, cache=True): "Memoizes; call .reset() to reset cache." if cache and mid in self.modelCache: return self.modelCache[mid] m = anki.models.Model(self, mid) if cache: self.modelCache[mid] = m return m # Utils ########################################################################## def nextID(self, type): type = "next"+type.capitalize() id = self.conf.get(type, 1) self.conf[type] = id+1 return id def reset(self): "Rebuild the queue and reload data after DB modified." self.modelCache = {} self.sched.reset() # Facts ########################################################################## def factCount(self): return self.db.scalar("select count() from facts where crt != 0") def newFact(self): "Return a new fact with the current model." return anki.facts.Fact(self, self.currentModel()) def addFact(self, fact): "Add a fact to the deck. Return number of new cards." # check we have card models available cms = self.findTemplates(fact) if not cms: return 0 # flush the fact fact.id = self.nextID("fid") fact.flush() # randomize? if self.randomNew(): due = random.randrange(1, fact.id) else: due = fact.id # add cards ncards = 0 for template in cms: self._newCard(fact, template, due) ncards += 1 return ncards def _delFacts(self, ids): "Bulk delete facts by ID. Don't call this directly." if not ids: return strids = ids2str(ids) self.db.execute("delete from facts where id in %s" % strids) self.db.execute("delete from fsums where fid in %s" % strids) def _delDanglingFacts(self): "Delete any facts without cards. Don't call this directly." ids = self.db.list(""" select id from facts where id not in (select distinct fid from cards)""") self._delFacts(ids) return ids # Card creation ########################################################################## def findTemplates(self, fact, checkActive=True): "Return (active), non-empty templates." ok = [] model = fact.model() for template in model.templates: if template['actv'] or not checkActive: # [cid, fid, mid, gid, ord, tags, flds] data = [1, 1, model.id, 1, template['ord'], "", fact.joinedFields()] now = self._renderQA(model, "", data) data[6] = "\x1f".join([""]*len(fact.fields)) empty = self._renderQA(model, "", data) if now['q'] == empty['q']: continue if not template['emptyAns']: if now['a'] == empty['a']: continue ok.append(template) return ok def genCards(self, fact, templates): "Generate cards for templates if cards not empty. Return cards." cards = [] # if random mode, determine insertion point if self.randomNew(): # if this fact has existing new cards, use their due time due = self.db.scalar( "select due from cards where fid = ? and queue = 0", fact.id) due = due or random.randrange(1, self.conf['nextFid']) else: due = fact.id for template in self.findTemplates(fact, checkActive=False): if template not in templates: continue # if it doesn't already exist if not self.db.scalar( "select 1 from cards where fid = ? and ord = ?", fact.id, template['ord']): # create cards.append(self._newCard(fact, template, due)) return cards # type 0 - when previewing in add dialog, only non-empty & active # type 1 - when previewing edit, only existing # type 2 - when previewing in models dialog, all def previewCards(self, fact, type=0): "Return uncommited cards for preview." if type == 0: cms = self.findTemplates(fact, checkActive=True) elif type == 1: cms = [c.template() for c in fact.cards()] else: cms = fact.model().templates if not cms: return [] cards = [] for template in cms: cards.append(self._newCard(fact, template, 1, flush=False)) return cards def _newCard(self, fact, template, due, flush=True): "Create a new card." card = anki.cards.Card(self) card.id = self.nextID("cid") card.fid = fact.id card.ord = template['ord'] card.gid = self.defaultGroup(template['gid'] or fact.gid) card.due = due if flush: card.flush() return card def randomNew(self): return self.qconf['newOrder'] == NEW_CARDS_RANDOM # Cards ########################################################################## def cardCount(self): return self.db.scalar("select count() from cards where crt != 0") def delCard(self, id): "Delete a card given its id. Delete any unused facts." self.delCards([id]) def delCards(self, ids): "Bulk delete cards by ID." if not ids: return sids = ids2str(ids) if self.schemaChanged(): # immediate delete? self.db.execute("delete from cards where id in %s" % sids) self.db.execute("delete from revlog where cid in %s" % sids) # remove any dangling facts self._delDanglingFacts() else: # trash sfids = ids2str( self.db.list("select fid from cards where id in "+sids)) # need to handle delete of fsums/revlog remotely after sync self.db.execute( "update cards set crt = 0, queue = -4, mod = ? where id in "+sids, intTime()) self.db.execute( "update facts set crt = 0, mod = ? where id in "+sfids, intTime()) self.db.execute("delete from fsums where fid in "+sfids) self.db.execute("delete from revlog where cid in "+sids) def emptyTrash(self): self.db.executescript(""" delete from facts where id in (select fid from cards where queue = -4); delete from cards where queue = -4;""") # Models ########################################################################## def currentModel(self): return self.getModel(self.conf['currentModelId']) def models(self): "Return a dict of mid -> model." mods = {} for m in [self.getModel(id) for id in self.db.list( "select id from models")]: mods[m.id] = m return mods def addModel(self, model): model.flush() self.conf['currentModelId'] = model.id def delModel(self, mid): "Delete MODEL, and all its cards/facts." # do a direct delete self.modSchema() # delete facts/cards self.delCards(self.db.list(""" select id from cards where fid in (select id from facts where mid = ?)""", mid)) # then the model self.db.execute("delete from models where id = ?", mid) # GUI should ensure last model is not deleted if self.conf['currentModelId'] == mid: self.conf['currentModelId'] = self.db.scalar( "select id from models limit 1") def allCSS(self): return "\n".join(self.db.list("select css from models")) # Field checksums and sorting fields ########################################################################## def _fieldData(self, sfids): return self.db.execute( "select id, mid, flds from facts where id in "+sfids) def updateFieldCache(self, fids, csum=True): "Update field checksums and sort cache, after find&replace, etc." sfids = ids2str(fids) mods = self.models() r = [] r2 = [] for (fid, mid, flds) in self._fieldData(sfids): fields = splitFields(flds) model = mods[mid] if csum: for f in model.fields: if f['uniq'] and fields[f['ord']]: r.append((fid, mid, fieldChecksum(fields[f['ord']]))) r2.append((stripHTML(fields[model.sortIdx()]), fid)) if csum: self.db.execute("delete from fsums where fid in "+sfids) self.db.executemany("insert into fsums values (?,?,?)", r) self.db.executemany("update facts set sfld = ? where id = ?", r2) # Q/A generation ########################################################################## def renderQA(self, ids=None, type="card"): # gather metadata if type == "card": where = "and c.id in " + ids2str(ids) elif type == "fact": where = "and f.id in " + ids2str(ids) elif type == "model": where = "and m.id in " + ids2str(ids) elif type == "all": where = "" else: raise Exception() mods = self.models() groups = dict(self.db.all("select id, name from groups")) return [self._renderQA(mods[row[2]], groups[row[3]], row) for row in self._qaData(where)] # fixme: don't need gid or data def _renderQA(self, model, gname, data): "Returns hash of id, question, answer." # data is [cid, fid, mid, gid, ord, tags, flds] # unpack fields and create dict flist = splitFields(data[6]) fields = {} for (name, (idx, conf)) in model.fieldMap().items(): fields[name] = flist[idx] if fields[name]: fields[name] = '<span class="fm%s-%s">%s</span>' % ( hexifyID(data[2]), hexifyID(idx), fields[name]) else: fields[name] = "" fields['Tags'] = data[5] fields['Model'] = model.name fields['Group'] = gname template = model.templates[data[4]] fields['Template'] = template['name'] # render q & a d = dict(id=data[0]) for (type, format) in (("q", template['qfmt']), ("a", template['afmt'])): if type == "q": format = format.replace("cloze:", "cq:") else: if model.conf['clozectx']: name = "cactx:" else: name = "ca:" format = format.replace("cloze:", name) fields = runFilter("mungeFields", fields, model, gname, data, self) html = anki.template.render(format, fields) d[type] = runFilter( "mungeQA", html, type, fields, model, gname, data, self) return d def _qaData(self, where=""): "Return [cid, fid, mid, gid, ord, tags, flds] db query" return self.db.execute(""" select c.id, f.id, f.mid, c.gid, c.ord, f.tags, f.flds from cards c, facts f where c.fid == f.id %s""" % where) # Tags ########################################################################## def tagList(self): return self.db.list("select name from tags order by name") def updateFactTags(self, fids=None): "Add any missing tags to the tags list." if fids: lim = " where id in " + ids2str(fids) else: lim = "" self.registerTags(set(parseTags( " ".join(self.db.list("select distinct tags from facts"+lim))))) def registerTags(self, tags): r = [] for t in tags: r.append({'t': t}) self.db.executemany(""" insert or ignore into tags (mod, name) values (%d, :t)""" % intTime(), r) def addTags(self, ids, tags, add=True): "Add tags in bulk. TAGS is space-separated." newTags = parseTags(tags) if not newTags: return # cache tag names self.registerTags(newTags) # find facts missing the tags if add: l = "tags not " fn = addTags else: l = "tags " fn = delTags lim = " or ".join( [l+"like :_%d" % c for c, t in enumerate(newTags)]) res = self.db.all( "select id, tags from facts where id in %s and %s" % ( ids2str(ids), lim), **dict([("_%d" % x, '%% %s %%' % y) for x, y in enumerate(newTags)])) # update tags fids = [] def fix(row): fids.append(row[0]) return {'id': row[0], 't': fn(tags, row[1]), 'n':intTime()} self.db.executemany(""" update facts set tags = :t, mod = :n where id = :id""", [fix(row) for row in res]) # update q/a cache self.registerTags(parseTags(tags)) def delTags(self, ids, tags): self.addTags(ids, tags, False) # Groups ########################################################################## def groups(self): "A list of all group names." return self.db.list("select name from groups order by name") def groupName(self, id): return self.db.scalar("select name from groups where id = ?", id) def groupId(self, name): "Return the id for NAME, creating if necessary." id = self.db.scalar("select id from groups where name = ?", name) if not id: id = self.db.execute( "insert into groups values (?,?,?,?, ?)", self.nextID("gid"), intTime(), name, 1, simplejson.dumps(anki.groups.defaultData)).lastrowid return id def defaultGroup(self, id): if id == 1: return 1 return self.db.scalar("select id from groups where id = ?", id) or 1 def delGroup(self, gid): self.modSchema() self.db.execute("update cards set gid = 1 where gid = ?", gid) self.db.execute("update facts set gid = 1 where gid = ?", gid) self.db.execute("delete from groups where id = ?", gid) def setGroup(self, cids, gid): self.db.execute( "update cards set gid = ? where id in "+ids2str(cids), gid) # Group configuration ########################################################################## def groupConfs(self): "Return [name, id]." return self.db.all("select name, id from gconf order by name") def groupConf(self, gcid): return simplejson.loads( self.db.scalar( "select conf from gconf where id = ?", gcid)) # Tag-based selective study ########################################################################## def selTagFids(self, yes, no): l = [] # find facts that match yes lim = "" args = [] query = "select id from facts" if not yes and not no: pass else: if yes: lim += " or ".join(["tags like ?" for t in yes]) args += ['%% %s %%' % t for t in yes] if no: lim2 = " and ".join(["tags not like ?" for t in no]) if lim: lim = "(%s) and %s" % (lim, lim2) else: lim = lim2 args += ['%% %s %%' % t for t in no] query += " where " + lim return self.db.list(query, *args) def setGroupForTags(self, yes, no, gid): fids = self.selTagFids(yes, no) self.db.execute( "update cards set gid = ? where fid in "+ids2str(fids), gid) # Finding cards ########################################################################## def findCards(self, query, full=False): return anki.find.Finder(self).findCards(query, full) def findReplace(self, fids, src, dst, regex=None, field=None, fold=True): return anki.find.findReplace(self, fids, src, dst, regex, field, fold) def findDuplicates(self, fmids): return anki.find.findDuplicates(self, fmids) # Stats ########################################################################## def cardStats(self, card): from anki.stats import CardStats return CardStats(self, card).report() def stats(self): from anki.stats import DeckStats return DeckStats(self) # Timeboxing ########################################################################## def startTimebox(self): self.lastSessionStart = self.sessionStartTime self.sessionStartTime = time.time() self.sessionStartReps = self.repsToday def stopTimebox(self): self.sessionStartTime = 0 def timeboxStarted(self): return self.sessionStartTime def timeboxReached(self): if not self.sessionStartTime: # not started return False if (self.sessionTimeLimit and time.time() > (self.sessionStartTime + self.sessionTimeLimit)): return True if (self.sessionRepLimit and self.sessionRepLimit <= self.repsToday - self.sessionStartReps): return True return False # Syncing ########################################################################## def enableSyncing(self): self.syncName = self.getSyncName() def disableSyncing(self): self.syncName = u"" def syncingEnabled(self): return not not self.syncName def genSyncName(self): return unicode(checksum(self.path.encode("utf-8"))) def syncHashBad(self): if self.syncName and self.syncName != self.genSyncName(): self.disableSyncing() return True # Schedulers and cramming ########################################################################## def stdSched(self): "True if scheduler changed." if self.sched.name != "std": self.cleanup() self.sched = self._stdSched return True def cramGroups(self, order="mod desc", min=0, max=None): self.stdSched() self.sched = anki.cram.CramScheduler(self, order, min, max) # Undo ########################################################################## def clearUndo(self): # [type, undoName, data] # type 1 = review; type 2 = checkpoint self._undo = None def undoName(self): "Undo menu item name, or None if undo unavailable." if not self._undo: return None return self._undo[1] def undo(self): if self._undo[0] == 1: self._undoReview() else: self._undoOp() def markReview(self, card): old = [] if self._undo: if self._undo[0] == 1: old = self._undo[2] self.clearUndo() self._undo = [1, _("Review"), old + [copy.copy(card)]] def _undoReview(self): data = self._undo[2] c = data.pop() if not data: self.clearUndo() # write old data c.flush() # and delete revlog entry last = self.db.scalar( "select time from revlog where cid = ? " "order by time desc limit 1", c.id) self.db.execute("delete from revlog where time = ?", last) def _markOp(self, name): "Call via .save()" if name: self._undo = [2, name] else: # saving disables old checkpoint, but not review undo if self._undo and self._undo[0] == 2: self.clearUndo() def _undoOp(self): self.rollback() self.clearUndo() # DB maintenance ########################################################################## def fixIntegrity(self): "Fix possible problems and rebuild caches." self.modSchema(check=False) problems = [] self.save() oldSize = os.stat(self.path)[stat.ST_SIZE] # tags self.db.execute("delete from tags") self.updateFactTags() # field cache for m in self.models().values(): self.updateFieldCache(m.fids()) # 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 optimize(self): self.db.execute("vacuum") self.db.execute("analyze") self.lock()