mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 07:22:23 -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.
150 lines
4.3 KiB
Python
150 lines
4.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.utils import intTime, hexifyID, timestampID
|
|
|
|
# Cards
|
|
##########################################################################
|
|
|
|
# Type: 0=new, 1=learning, 2=due
|
|
# Queue: same as above, and:
|
|
# -1=suspended, -2=user buried, -3=sched buried
|
|
# Due is used differently for different queues.
|
|
# - new queue: fact id or random int
|
|
# - rev queue: integer day
|
|
# - lrn queue: integer timestamp
|
|
|
|
class Card(object):
|
|
|
|
def __init__(self, deck, id=None):
|
|
self.deck = deck
|
|
self.timerStarted = None
|
|
self._qa = None
|
|
self._rd = None
|
|
if id:
|
|
self.id = id
|
|
self.load()
|
|
else:
|
|
# to flush, set fid, ord, and due
|
|
self.id = timestampID(deck.db, "cards")
|
|
self.gid = 1
|
|
self.crt = intTime()
|
|
self.type = 0
|
|
self.queue = 0
|
|
self.ivl = 0
|
|
self.factor = 0
|
|
self.reps = 0
|
|
self.lapses = 0
|
|
self.grade = 0
|
|
self.cycles = 0
|
|
self.edue = 0
|
|
self.data = ""
|
|
|
|
def load(self):
|
|
(self.id,
|
|
self.fid,
|
|
self.gid,
|
|
self.ord,
|
|
self.mod,
|
|
self.usn,
|
|
self.type,
|
|
self.queue,
|
|
self.due,
|
|
self.ivl,
|
|
self.factor,
|
|
self.reps,
|
|
self.lapses,
|
|
self.grade,
|
|
self.cycles,
|
|
self.edue,
|
|
self.data) = self.deck.db.first(
|
|
"select * from cards where id = ?", self.id)
|
|
self._qa = None
|
|
self._rd = None
|
|
|
|
def flush(self):
|
|
self.mod = intTime()
|
|
self.usn = self.deck.usn()
|
|
self.deck.db.execute(
|
|
"""
|
|
insert or replace into cards values
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
self.id,
|
|
self.fid,
|
|
self.gid,
|
|
self.ord,
|
|
self.mod,
|
|
self.usn,
|
|
self.type,
|
|
self.queue,
|
|
self.due,
|
|
self.ivl,
|
|
self.factor,
|
|
self.reps,
|
|
self.lapses,
|
|
self.grade,
|
|
self.cycles,
|
|
self.edue,
|
|
self.data)
|
|
|
|
def flushSched(self):
|
|
self.mod = intTime()
|
|
self.usn = self.deck.usn()
|
|
self.deck.db.execute(
|
|
"""update cards set
|
|
mod=?, usn=?, type=?, queue=?, due=?, ivl=?, factor=?, reps=?,
|
|
lapses=?, grade=?, cycles=?, edue=? where id = ?""",
|
|
self.mod, self.usn, self.type, self.queue, self.due, self.ivl,
|
|
self.factor, self.reps, self.lapses,
|
|
self.grade, self.cycles, self.edue, self.id)
|
|
|
|
def q(self, classes="q", reload=False):
|
|
return self._withClass(self._getQA(reload)['q'], classes)
|
|
|
|
def a(self, classes="a"):
|
|
return self._withClass(self._getQA()['a'], classes)
|
|
|
|
def _getQA(self, reload=False):
|
|
if not self._qa or reload:
|
|
f = self.fact(); m = self.model()
|
|
data = [self.id, f.id, m['id'], self.gid, self.ord, f.stringTags(),
|
|
f.joinedFields()]
|
|
self._qa = self.deck._renderQA(data)
|
|
return self._qa
|
|
|
|
def _withClass(self, txt, extra):
|
|
return '<div class="%s %s">%s</div>' % (self.cssClass(), extra, txt)
|
|
|
|
def _reviewData(self, reload=False):
|
|
"Fetch the model and fact."
|
|
if not self._rd or reload:
|
|
f = self.deck.getFact(self.fid)
|
|
m = self.deck.models.get(f.mid)
|
|
self._rd = [f, m]
|
|
return self._rd
|
|
|
|
def fact(self):
|
|
return self._reviewData()[0]
|
|
|
|
def model(self, reload=False):
|
|
return self._reviewData()[1]
|
|
|
|
def groupConf(self):
|
|
return self.deck.groups.conf(self.gid)
|
|
|
|
def template(self):
|
|
return self._reviewData()[1]['tmpls'][self.ord]
|
|
|
|
def cssClass(self):
|
|
return "cm%s-%s" % (hexifyID(self.model()['id']),
|
|
hexifyID(self.template()['ord']))
|
|
|
|
def startTimer(self):
|
|
self.timerStarted = time.time()
|
|
|
|
def timeTaken(self):
|
|
"Time taken to answer card, in integer MS."
|
|
total = int((time.time() - self.timerStarted)*1000)
|
|
return min(total, self.groupConf()['maxTaken']*1000)
|