mirror of
https://github.com/ankitects/anki.git
synced 2025-11-08 13:47:13 -05:00
Syncing and shared decks have conflicting priorities: - For syncing, we need to ensure that the deck remains in a consistent state. In the past, Anki allowed deletions to be overriden by a more recently modified object, but this could lead to a broken deck in certain circumstances. For example, if a user deletes a fact (and its cards) on one side, but does something to bump a card's mod time on another side, then when syncing the card would be brought back to life without its fact. Short of complex code to check all the relations, we're limited to two options: forcing a full sync when things are deleted, or ensuring objects can't come back to life. - When facts are shared between people, we need a way to identify if two facts arose from the same source. We can't compare based on content, as the content may have changed partially or completely. And we can't use the timestamp ids because of the above restriction on bringing objects back to life. If we did that, people could download a shared deck, decide they don't want it, and delete it. When they later decide to add it again, it wouldn't be possible: either nothing would be imported because of the old graves, or the ids would have to be rewritten. If we do the latter, the facts are no longer associated with each other, and we lose the ability to update the deck. So we need to give facts two IDs: one used as the primary key and for syncing, and another 'global id' for importing/sharing. I used a 64 bit random number, because a) it's what Anki's used in the past, so by reusing the old IDs we don't break existing associations on upgrade, and b) it's a decent compromise between the possibility of conflicts and performance. Also re-added a flags column to the facts. The 'data' column is intended to store JSON in the future for extra features without changing the schema, but that's slow for simple state checks. Flags will be used as a bitmask.
179 lines
5.5 KiB
Python
179 lines
5.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import time
|
|
from anki.errors import AnkiError
|
|
from anki.utils import fieldChecksum, intTime, \
|
|
joinFields, splitFields, ids2str, stripHTML, timestampID, guid64
|
|
|
|
class Fact(object):
|
|
|
|
def __init__(self, deck, model=None, id=None):
|
|
assert not (model and id)
|
|
self.deck = deck
|
|
if id:
|
|
self.id = id
|
|
self.load()
|
|
else:
|
|
self.id = timestampID(deck.db, "facts")
|
|
self.guid = guid64()
|
|
self._model = model
|
|
self.gid = model['gid']
|
|
self.mid = model['id']
|
|
self.tags = []
|
|
self.fields = [""] * len(self._model['flds'])
|
|
self.flags = 0
|
|
self.data = ""
|
|
self._fmap = self.deck.models.fieldMap(self._model)
|
|
|
|
def load(self):
|
|
(self.guid,
|
|
self.mid,
|
|
self.gid,
|
|
self.mod,
|
|
self.usn,
|
|
self.tags,
|
|
self.fields,
|
|
self.flags,
|
|
self.data) = self.deck.db.first("""
|
|
select guid, mid, gid, mod, usn, tags, flds, flags, data
|
|
from facts where id = ?""", self.id)
|
|
self.fields = splitFields(self.fields)
|
|
self.tags = self.deck.tags.split(self.tags)
|
|
self._model = self.deck.models.get(self.mid)
|
|
self._fmap = self.deck.models.fieldMap(self._model)
|
|
|
|
def flush(self, mod=None):
|
|
self.mod = mod if mod else intTime()
|
|
self.usn = self.deck.usn()
|
|
sfld = stripHTML(self.fields[self.deck.models.sortIdx(self._model)])
|
|
tags = self.stringTags()
|
|
res = self.deck.db.execute("""
|
|
insert or replace into facts values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
self.id, self.guid, self.mid, self.gid,
|
|
self.mod, self.usn, tags,
|
|
self.joinedFields(), sfld, self.flags, self.data)
|
|
self.id = res.lastrowid
|
|
self.updateFieldChecksums()
|
|
self.deck.tags.register(self.tags)
|
|
|
|
def joinedFields(self):
|
|
return joinFields(self.fields)
|
|
|
|
def updateFieldChecksums(self):
|
|
self.deck.db.execute("delete from fsums where fid = ?", self.id)
|
|
d = []
|
|
for (ord, conf) in self._fmap.values():
|
|
if not conf['uniq']:
|
|
continue
|
|
val = self.fields[ord]
|
|
if not val:
|
|
continue
|
|
d.append((self.id, self.mid, fieldChecksum(val)))
|
|
self.deck.db.executemany("insert into fsums values (?, ?, ?)", d)
|
|
|
|
def cards(self):
|
|
return [self.deck.getCard(id) for id in self.deck.db.list(
|
|
"select id from cards where fid = ? order by ord", self.id)]
|
|
|
|
def model(self):
|
|
return self._model
|
|
|
|
def updateCardGids(self):
|
|
for c in self.cards():
|
|
if c.gid != self.gid and not c.template()['gid']:
|
|
c.gid = self.gid
|
|
c.flush()
|
|
|
|
# Dict interface
|
|
##################################################
|
|
|
|
def keys(self):
|
|
return self._fmap.keys()
|
|
|
|
def values(self):
|
|
return self.fields
|
|
|
|
def items(self):
|
|
return [(f['name'], self.fields[ord])
|
|
for ord, f in sorted(self._fmap.values())]
|
|
|
|
def _fieldOrd(self, key):
|
|
try:
|
|
return self._fmap[key][0]
|
|
except:
|
|
raise KeyError(key)
|
|
|
|
def __getitem__(self, key):
|
|
return self.fields[self._fieldOrd(key)]
|
|
|
|
def __setitem__(self, key, value):
|
|
self.fields[self._fieldOrd(key)] = value
|
|
|
|
# Tags
|
|
##################################################
|
|
|
|
def hasTag(self, tag):
|
|
return self.deck.tags.inList(tag, self.tags)
|
|
|
|
def stringTags(self):
|
|
return self.deck.tags.canonify(self.tags)
|
|
|
|
def delTag(self, tag):
|
|
rem = []
|
|
for t in self.tags:
|
|
if t.lower() == tag.lower():
|
|
rem.append(t)
|
|
for r in rem:
|
|
self.tags.remove(r)
|
|
|
|
def addTag(self, tag):
|
|
# duplicates will be stripped on save
|
|
self.tags.append(tag)
|
|
|
|
# Unique/duplicate checks
|
|
##################################################
|
|
|
|
def fieldUnique(self, name):
|
|
(ord, conf) = self._fmap[name]
|
|
if not conf['uniq']:
|
|
return True
|
|
val = self[name]
|
|
if not val:
|
|
return True
|
|
csum = fieldChecksum(val)
|
|
if self.id:
|
|
lim = "and fid != :fid"
|
|
else:
|
|
lim = ""
|
|
fids = self.deck.db.list(
|
|
"select fid from fsums where csum = ? and fid != ? and mid = ?",
|
|
csum, self.id or 0, self.mid)
|
|
if not fids:
|
|
return True
|
|
# grab facts with the same checksums, and see if they're actually
|
|
# duplicates
|
|
for flds in self.deck.db.list("select flds from facts where id in "+
|
|
ids2str(fids)):
|
|
fields = splitFields(flds)
|
|
if fields[ord] == val:
|
|
return False
|
|
return True
|
|
|
|
def fieldComplete(self, name, text=None):
|
|
(ord, conf) = self._fmap[name]
|
|
if not conf['req']:
|
|
return True
|
|
return self[name]
|
|
|
|
def problems(self):
|
|
d = []
|
|
for (k, (ord, conf)) in self._fmap.items():
|
|
if not self.fieldUnique(k):
|
|
d.append((ord, "unique"))
|
|
elif not self.fieldComplete(k):
|
|
d.append((ord, "required"))
|
|
else:
|
|
d.append((ord, None))
|
|
return [x[1] for x in sorted(d)]
|