mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00

Decks now have an "update sequence number". All objects also have a USN, which is set to the deck USN each time they are modified. When syncing, each side sends any objects with a USN >= clientUSN. When objects are copied via sync, they have their USNs bumped to the current serverUSN. After a sync, the USN on both sides is set to serverUSN + 1. This solves the failing three way test, ensures we receive all changes regardless of clock drift, and as the revlog also has a USN now, ensures that old revlog entries are imported properly too. Objects retain a separate modification time, which is used for conflict resolution, deck subscriptions/importing, and info for the user. Note that if the clock is too far off, it will still cause confusion for users, as the due counts may be different depending on the time. For this reason it's probably a good idea to keep a limit on how far the clock can deviate. We still keep track of the last sync time, but only so we can determine if the schema has changed since the last sync. The media code needs to be updated to use USNs too.
174 lines
5.3 KiB
Python
174 lines
5.3 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
|
|
|
|
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._model = model
|
|
self.gid = model['gid']
|
|
self.mid = model['id']
|
|
self.tags = []
|
|
self.fields = [""] * len(self._model['flds'])
|
|
self.data = ""
|
|
self._fmap = self.deck.models.fieldMap(self._model)
|
|
|
|
def load(self):
|
|
(self.mid,
|
|
self.gid,
|
|
self.mod,
|
|
self.usn,
|
|
self.tags,
|
|
self.fields,
|
|
self.data) = self.deck.db.first("""
|
|
select mid, gid, mod, usn, tags, flds, 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.mid, self.gid,
|
|
self.mod, self.usn, tags,
|
|
self.joinedFields(), sfld, 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)]
|