mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -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.
218 lines
6.3 KiB
Python
218 lines
6.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 simplejson
|
|
from anki.utils import intTime
|
|
from anki.consts import *
|
|
|
|
# fixmes:
|
|
# - make sure lists like new[delays] are not being shared by multiple groups
|
|
# - make sure all children have parents (create as necessary)
|
|
# - when renaming a group, top level properties should be added or removed as
|
|
# appropriate
|
|
|
|
# configuration only available to top level groups
|
|
defaultTopConf = {
|
|
'newPerDay': 20,
|
|
'newToday': [0, 0], # currentDay, count
|
|
'revToday': [0, 0],
|
|
'lrnToday': [0, 0],
|
|
'timeToday': [0, 0], # currentDay, time in ms
|
|
'newTodayOrder': NEW_TODAY_ORD,
|
|
'newSpread': NEW_CARDS_DISTRIBUTE,
|
|
'collapseTime': 1200,
|
|
'repLim': 0,
|
|
'timeLim': 600,
|
|
'curModel': None,
|
|
}
|
|
|
|
# configuration available to all groups
|
|
defaultConf = {
|
|
'new': {
|
|
'delays': [1, 10],
|
|
'ints': [1, 7, 4],
|
|
'initialFactor': 2500,
|
|
},
|
|
'lapse': {
|
|
'delays': [1, 10],
|
|
'mult': 0,
|
|
'minInt': 1,
|
|
'relearn': True,
|
|
'leechFails': 8,
|
|
# type 0=suspend, 1=tagonly
|
|
'leechAction': 0,
|
|
},
|
|
'cram': {
|
|
'delays': [1, 5, 10],
|
|
'resched': True,
|
|
'reset': True,
|
|
'mult': 0,
|
|
'minInt': 1,
|
|
},
|
|
'rev': {
|
|
'ease4': 1.3,
|
|
'fuzz': 0.05,
|
|
'minSpace': 1,
|
|
},
|
|
'maxTaken': 60,
|
|
'mod': 0,
|
|
'usn': 0,
|
|
}
|
|
|
|
class GroupManager(object):
|
|
|
|
# Registry save/load
|
|
#############################################################
|
|
|
|
def __init__(self, deck):
|
|
self.deck = deck
|
|
|
|
def load(self, groups, gconf):
|
|
self.groups = simplejson.loads(groups)
|
|
self.gconf = simplejson.loads(gconf)
|
|
self.changed = False
|
|
|
|
def save(self, g=None):
|
|
"Can be called with either a group or a group configuration."
|
|
if g:
|
|
g['mod'] = intTime()
|
|
g['usn'] = self.deck.usn()
|
|
self.changed = True
|
|
|
|
def flush(self):
|
|
if self.changed:
|
|
self.deck.db.execute("update deck set groups=?, gconf=?",
|
|
simplejson.dumps(self.groups),
|
|
simplejson.dumps(self.gconf))
|
|
|
|
# Group save/load
|
|
#############################################################
|
|
|
|
def id(self, name, create=True):
|
|
"Add a group with NAME. Reuse group if already exists. Return id as int."
|
|
for id, g in self.groups.items():
|
|
if g['name'].lower() == name.lower():
|
|
return int(id)
|
|
if not create:
|
|
return None
|
|
if "::" not in name:
|
|
# if it's a top level group, it gets the top level config
|
|
g = defaultTopConf.copy()
|
|
else:
|
|
# not top level; ensure all parents exist
|
|
g = {}
|
|
self._ensureParents(name)
|
|
g['name'] = name
|
|
g['conf'] = 1
|
|
while 1:
|
|
id = intTime(1000)
|
|
if str(id) in self.groups:
|
|
continue
|
|
g['id'] = id
|
|
self.groups[str(id)] = g
|
|
self.save(g)
|
|
return int(id)
|
|
|
|
def rem(self, gid):
|
|
self.deck.modSchema()
|
|
self.deck.db.execute(
|
|
"update cards set gid=1,usn=?,mod=? where gid = ?",
|
|
gid, self.deck.usn(), intTime())
|
|
self.deck.db.execute(
|
|
"update facts set gid=1,usn=?,mod=? where gid = ?",
|
|
gid, self.deck.usn(), intTime())
|
|
self.deck.db.execute("delete from groups where id = ?", gid)
|
|
print "fixme: loop through models and update stale gid references"
|
|
|
|
def allNames(self):
|
|
"An unsorted list of all group names."
|
|
return [x['name'] for x in self.groups.values()]
|
|
|
|
def all(self):
|
|
"A list of all groups."
|
|
return self.groups.values()
|
|
|
|
def allConf(self):
|
|
"A list of all group config."
|
|
return self.gconf.values()
|
|
|
|
def _ensureParents(self, name):
|
|
path = name.split("::")
|
|
s = ""
|
|
for p in path[:-1]:
|
|
if not s:
|
|
s += p
|
|
else:
|
|
s += "::" + p
|
|
self.id(s)
|
|
|
|
# Group utils
|
|
#############################################################
|
|
|
|
def name(self, gid):
|
|
return self.groups[str(gid)]['name']
|
|
|
|
def conf(self, gid):
|
|
return self.gconf[str(self.groups[str(gid)]['conf'])]
|
|
|
|
def get(self, gid):
|
|
id = str(gid)
|
|
if id in self.groups:
|
|
return self.groups[id]
|
|
|
|
def setGroup(self, cids, gid):
|
|
self.db.execute(
|
|
"update cards set gid=?,usn=?,mod=? where id in "+
|
|
ids2str(cids), gid, self.deck.usn(), intTime())
|
|
|
|
def update(self, g):
|
|
"Add or update an existing group. Used for syncing and merging."
|
|
self.groups[str(g['id'])] = g
|
|
# mark registry changed, but don't bump mod time
|
|
self.save()
|
|
|
|
def updateConf(self, g):
|
|
self.gconf[str(g['id'])] = g
|
|
self.save()
|
|
|
|
# Group selection
|
|
#############################################################
|
|
|
|
def top(self):
|
|
"The current top level group as an object, and marks as modified."
|
|
g = self.get(self.deck.conf['topGroup'])
|
|
self.save(g)
|
|
return g
|
|
|
|
def active(self):
|
|
"The currrently active gids."
|
|
return self.deck.conf['activeGroups']
|
|
|
|
def selected(self):
|
|
"The currently selected gid, or None if whole collection."
|
|
return self.deck.conf['curGroup']
|
|
|
|
def select(self, gid):
|
|
"Select a new group. If gid is None, select whole collection."
|
|
if not gid:
|
|
self.deck.conf['topGroup'] = 1
|
|
self.deck.conf['curGroup'] = None
|
|
self.deck.conf['activeGroups'] = []
|
|
return
|
|
# save the top level group
|
|
name = self.groups[str(gid)]['name']
|
|
self.deck.conf['topGroup'] = self.topFor(name)
|
|
# current group
|
|
self.deck.conf['curGroup'] = gid
|
|
# and active groups (current + all children)
|
|
actv = [gid]
|
|
for g in self.all():
|
|
if g['name'].startswith(name + "::"):
|
|
actv.append(g['id'])
|
|
self.deck.conf['activeGroups'] = actv
|
|
|
|
def topFor(self, name):
|
|
"The top level gid for NAME."
|
|
path = name.split("::")
|
|
return self.id(path[0])
|