preserve mtime/usn when syncing deck config, and add snake_case names

This commit is contained in:
Damien Elmes 2020-04-03 13:54:52 +10:00
parent d5f6d8b476
commit 333d0735ff
13 changed files with 90 additions and 48 deletions

View file

@ -51,7 +51,7 @@ message BackendInput {
Card update_card = 39; Card update_card = 39;
Card add_card = 40; Card add_card = 40;
int64 get_deck_config = 41; int64 get_deck_config = 41;
string add_or_update_deck_config = 42; AddOrUpdateDeckConfigIn add_or_update_deck_config = 42;
Empty all_deck_config = 43; Empty all_deck_config = 43;
Empty new_deck_config = 44; Empty new_deck_config = 44;
int64 remove_deck_config = 45; int64 remove_deck_config = 45;
@ -414,3 +414,8 @@ message Card {
message CloseCollectionIn { message CloseCollectionIn {
bool downgrade_to_schema11 = 1; bool downgrade_to_schema11 = 1;
} }
message AddOrUpdateDeckConfigIn {
string config = 1;
bool preserve_usn_and_mtime = 2;
}

View file

@ -74,7 +74,7 @@ class DeckManager:
if g: if g:
# deck conf? # deck conf?
if "maxTaken" in g: if "maxTaken" in g:
self.updateConf(g) self.update_config(g)
return return
else: else:
g["mod"] = intTime() g["mod"] = intTime()
@ -318,7 +318,7 @@ class DeckManager:
# Deck configurations # Deck configurations
############################################################# #############################################################
def allConf(self) -> List: def all_config(self) -> List:
"A list of all deck config." "A list of all deck config."
return list(self.col.backend.all_deck_config()) return list(self.col.backend.all_deck_config())
@ -327,32 +327,38 @@ class DeckManager:
assert deck assert deck
if "conf" in deck: if "conf" in deck:
dcid = int(deck["conf"]) # may be a string dcid = int(deck["conf"]) # may be a string
conf = self.getConf(dcid) conf = self.get_config(dcid)
conf["dyn"] = False conf["dyn"] = False
return conf return conf
# dynamic decks have embedded conf # dynamic decks have embedded conf
return deck return deck
def getConf(self, confId: int) -> Any: def get_config(self, conf_id: int) -> Any:
if self._dconf_cache is not None: if self._dconf_cache is not None:
return self._dconf_cache.get(confId) return self._dconf_cache.get(conf_id)
return self.col.backend.get_deck_config(confId) return self.col.backend.get_deck_config(conf_id)
def updateConf(self, g: Dict[str, Any]) -> None: def update_config(self, conf: Dict[str, Any], preserve_usn=False) -> None:
self.col.backend.add_or_update_deck_config(g) self.col.backend.add_or_update_deck_config(conf, preserve_usn)
def confId(self, name: str, cloneFrom: Optional[Dict[str, Any]] = None) -> int: def add_config(
"Create a new configuration and return id." self, name: str, clone_from: Optional[Dict[str, Any]] = None
if cloneFrom is not None: ) -> Dict[str, Any]:
conf = copy.deepcopy(cloneFrom) if clone_from is not None:
conf = copy.deepcopy(clone_from)
conf["id"] = 0 conf["id"] = 0
else: else:
conf = self.col.backend.new_deck_config() conf = self.col.backend.new_deck_config()
conf["name"] = name conf["name"] = name
self.updateConf(conf) self.update_config(conf)
return conf["id"] return conf
def remConf(self, id) -> None: def add_config_returning_id(
self, name: str, clone_from: Optional[Dict[str, Any]] = None
) -> int:
return self.add_config(name, clone_from)["id"]
def remove_config(self, id) -> None:
"Remove a configuration and update all decks using it." "Remove a configuration and update all decks using it."
self.col.modSchema(check=True) self.col.modSchema(check=True)
for g in self.all(): for g in self.all():
@ -380,14 +386,21 @@ class DeckManager:
new = self.col.backend.new_deck_config() new = self.col.backend.new_deck_config()
new["id"] = conf["id"] new["id"] = conf["id"]
new["name"] = conf["name"] new["name"] = conf["name"]
self.updateConf(new) self.update_config(new)
# if it was previously randomized, re-sort # if it was previously randomized, re-sort
if not oldOrder: if not oldOrder:
self.col.sched.resortConf(new) self.col.sched.resortConf(new)
# legacy
allConf = all_config
getConf = get_config
updateConf = update_config
remConf = remove_config
confId = add_config_returning_id
# temporary caching - don't use this as it will be removed # temporary caching - don't use this as it will be removed
def _enable_dconf_cache(self): def _enable_dconf_cache(self):
self._dconf_cache = {c["id"]: c for c in self.allConf()} self._dconf_cache = {c["id"]: c for c in self.all_config()}
def _disable_dconf_cache(self): def _disable_dconf_cache(self):
self._dconf_cache = None self._dconf_cache = None
@ -613,8 +626,10 @@ class DeckManager:
def beforeUpload(self) -> None: def beforeUpload(self) -> None:
for d in self.all(): for d in self.all():
d["usn"] = 0 d["usn"] = 0
for c in self.allConf(): for c in self.all_config():
c["usn"] = 0 if c["usn"] != 0:
c["usn"] = 0
self.update_config(c, preserve_usn=True)
self.save() self.save()
# Dynamic decks # Dynamic decks

View file

@ -249,7 +249,7 @@ class AnkiExporter(Exporter):
# copy used deck confs # copy used deck confs
for dc in self.src.decks.allConf(): for dc in self.src.decks.allConf():
if dc["id"] in dconfs: if dc["id"] in dconfs:
self.dst.decks.updateConf(dc) self.dst.decks.update_config(dc)
# find used media # find used media
media = {} media = {}
self.mediaDir = self.src.media.dir() self.mediaDir = self.src.media.dir()

View file

@ -277,9 +277,9 @@ class Anki2Importer(Importer):
newid = self.dst.decks.id(name) newid = self.dst.decks.id(name)
# pull conf over # pull conf over
if "conf" in g and g["conf"] != 1: if "conf" in g and g["conf"] != 1:
conf = self.src.decks.getConf(g["conf"]) conf = self.src.decks.get_config(g["conf"])
self.dst.decks.save(conf) self.dst.decks.save(conf)
self.dst.decks.updateConf(conf) self.dst.decks.update_config(conf)
g2 = self.dst.decks.get(newid) g2 = self.dst.decks.get(newid)
g2["conf"] = g["conf"] g2["conf"] = g["conf"]
self.dst.decks.save(g2) self.dst.decks.save(g2)

View file

@ -2,6 +2,15 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# pylint: skip-file # pylint: skip-file
"""
Python bindings for Anki's Rust libraries.
Please do not access methods on the backend directly - they may be changed
or removed at any time. Instead, please use the methods on the collection
instead. Eg, don't use col.backend.all_deck_config(), instead use
col.decks.all_config()
"""
import enum import enum
import json import json
import os import os
@ -502,10 +511,14 @@ class RustBackend:
jstr = self._run_command(pb.BackendInput(get_deck_config=dcid)).get_deck_config jstr = self._run_command(pb.BackendInput(get_deck_config=dcid)).get_deck_config
return json.loads(jstr) return json.loads(jstr)
def add_or_update_deck_config(self, conf: Dict[str, Any]) -> None: def add_or_update_deck_config(self, conf: Dict[str, Any], preserve_usn) -> None:
conf_json = json.dumps(conf) conf_json = json.dumps(conf)
id = self._run_command( id = self._run_command(
pb.BackendInput(add_or_update_deck_config=conf_json) pb.BackendInput(
add_or_update_deck_config=pb.AddOrUpdateDeckConfigIn(
config=conf_json, preserve_usn_and_mtime=preserve_usn
)
)
).add_or_update_deck_config ).add_or_update_deck_config
conf["id"] = id conf["id"] = id

View file

@ -374,9 +374,10 @@ from notes where %s"""
decks = [g for g in self.col.decks.all() if g["usn"] == -1] decks = [g for g in self.col.decks.all() if g["usn"] == -1]
for g in decks: for g in decks:
g["usn"] = self.maxUsn g["usn"] = self.maxUsn
dconf = [g for g in self.col.decks.allConf() if g["usn"] == -1] dconf = [g for g in self.col.decks.all_config() if g["usn"] == -1]
for g in dconf: for g in dconf:
g["usn"] = self.maxUsn g["usn"] = self.maxUsn
self.col.decks.update_config(g, preserve_usn=True)
self.col.decks.save() self.col.decks.save()
return [decks, dconf] return [decks, dconf]
@ -392,12 +393,12 @@ from notes where %s"""
self.col.decks.update(r) self.col.decks.update(r)
for r in rchg[1]: for r in rchg[1]:
try: try:
l = self.col.decks.getConf(r["id"]) l = self.col.decks.get_config(r["id"])
except KeyError: except KeyError:
l = None l = None
# if missing locally or server is newer, update # if missing locally or server is newer, update
if not l or r["mod"] > l["mod"]: if not l or r["mod"] > l["mod"]:
self.col.decks.updateConf(r) self.col.decks.update_config(r)
# Tags # Tags
########################################################################## ##########################################################################

View file

@ -45,8 +45,8 @@ def test_export_anki():
# create a new deck with its own conf to test conf copying # create a new deck with its own conf to test conf copying
did = deck.decks.id("test") did = deck.decks.id("test")
dobj = deck.decks.get(did) dobj = deck.decks.get(did)
confId = deck.decks.confId("newconf") confId = deck.decks.add_config_returning_id("newconf")
conf = deck.decks.getConf(confId) conf = deck.decks.get_config(confId)
conf["new"]["perDay"] = 5 conf["new"]["perDay"] = 5
deck.decks.save(conf) deck.decks.save(conf)
deck.decks.setConf(dobj, confId) deck.decks.setConf(dobj, confId)

View file

@ -90,7 +90,7 @@ def test_newLimits():
f.model()["did"] = g2 f.model()["did"] = g2
d.addNote(f) d.addNote(f)
# give the child deck a different configuration # give the child deck a different configuration
c2 = d.decks.confId("new conf") c2 = d.decks.add_config_returning_id("new conf")
d.decks.setConf(d.decks.get(g2), c2) d.decks.setConf(d.decks.get(g2), c2)
d.reset() d.reset()
# both confs have defaulted to a limit of 20 # both confs have defaulted to a limit of 20

View file

@ -90,7 +90,7 @@ def test_newLimits():
f.model()["did"] = g2 f.model()["did"] = g2
d.addNote(f) d.addNote(f)
# give the child deck a different configuration # give the child deck a different configuration
c2 = d.decks.confId("new conf") c2 = d.decks.add_config_returning_id("new conf")
d.decks.setConf(d.decks.get(g2), c2) d.decks.setConf(d.decks.get(g2), c2)
d.reset() d.reset()
# both confs have defaulted to a limit of 20 # both confs have defaulted to a limit of 20
@ -412,14 +412,14 @@ def test_review_limits():
parent = d.decks.get(d.decks.id("parent")) parent = d.decks.get(d.decks.id("parent"))
child = d.decks.get(d.decks.id("parent::child")) child = d.decks.get(d.decks.id("parent::child"))
pconf = d.decks.getConf(d.decks.confId("parentConf")) pconf = d.decks.get_config(d.decks.add_config_returning_id("parentConf"))
cconf = d.decks.getConf(d.decks.confId("childConf")) cconf = d.decks.get_config(d.decks.add_config_returning_id("childConf"))
pconf["rev"]["perDay"] = 5 pconf["rev"]["perDay"] = 5
d.decks.updateConf(pconf) d.decks.update_config(pconf)
d.decks.setConf(parent, pconf["id"]) d.decks.setConf(parent, pconf["id"])
cconf["rev"]["perDay"] = 10 cconf["rev"]["perDay"] = 10
d.decks.updateConf(cconf) d.decks.update_config(cconf)
d.decks.setConf(child, cconf["id"]) d.decks.setConf(child, cconf["id"])
m = d.models.current() m = d.models.current()

View file

@ -26,7 +26,7 @@ class CustomStudy(QDialog):
QDialog.__init__(self, mw) QDialog.__init__(self, mw)
self.mw = mw self.mw = mw
self.deck = self.mw.col.decks.current() self.deck = self.mw.col.decks.current()
self.conf = self.mw.col.decks.getConf(self.deck["conf"]) self.conf = self.mw.col.decks.get_config(self.deck["conf"])
self.form = f = aqt.forms.customstudy.Ui_Dialog() self.form = f = aqt.forms.customstudy.Ui_Dialog()
self.created_custom_study = False self.created_custom_study = False
f.setupUi(self) f.setupUi(self)

View file

@ -123,7 +123,7 @@ class DeckConf(QDialog):
# first, save currently entered data to current conf # first, save currently entered data to current conf
self.saveConf() self.saveConf()
# then clone the conf # then clone the conf
id = self.mw.col.decks.confId(name, cloneFrom=self.conf) id = self.mw.col.decks.add_config_returning_id(name, cloneFrom=self.conf)
# set the deck to the new conf # set the deck to the new conf
self.deck["conf"] = id self.deck["conf"] = id
# then reload the conf list # then reload the conf list
@ -133,7 +133,7 @@ class DeckConf(QDialog):
if int(self.conf["id"]) == 1: if int(self.conf["id"]) == 1:
showInfo(_("The default configuration can't be removed."), self) showInfo(_("The default configuration can't be removed."), self)
else: else:
self.mw.col.decks.remConf(self.conf["id"]) self.mw.col.decks.remove_config(self.conf["id"])
self.deck["conf"] = 1 self.deck["conf"] = 1
self.loadConfs() self.loadConfs()

View file

@ -3,7 +3,9 @@
use crate::backend::dbproxy::db_command_bytes; use crate::backend::dbproxy::db_command_bytes;
use crate::backend_proto::backend_input::Value; use crate::backend_proto::backend_input::Value;
use crate::backend_proto::{BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn}; use crate::backend_proto::{
AddOrUpdateDeckConfigIn, BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn,
};
use crate::card::{Card, CardID}; use crate::card::{Card, CardID};
use crate::card::{CardQueue, CardType}; use crate::card::{CardQueue, CardType};
use crate::collection::{open_collection, Collection}; use crate::collection::{open_collection, Collection};
@ -268,8 +270,8 @@ impl Backend {
} }
Value::AddCard(card) => OValue::AddCard(self.add_card(card)?), Value::AddCard(card) => OValue::AddCard(self.add_card(card)?),
Value::GetDeckConfig(dcid) => OValue::GetDeckConfig(self.get_deck_config(dcid)?), Value::GetDeckConfig(dcid) => OValue::GetDeckConfig(self.get_deck_config(dcid)?),
Value::AddOrUpdateDeckConfig(conf_json) => { Value::AddOrUpdateDeckConfig(input) => {
OValue::AddOrUpdateDeckConfig(self.add_or_update_deck_config(conf_json)?) OValue::AddOrUpdateDeckConfig(self.add_or_update_deck_config(input)?)
} }
Value::AllDeckConfig(_) => OValue::AllDeckConfig(self.all_deck_config()?), Value::AllDeckConfig(_) => OValue::AllDeckConfig(self.all_deck_config()?),
Value::NewDeckConfig(_) => OValue::NewDeckConfig(self.new_deck_config()?), Value::NewDeckConfig(_) => OValue::NewDeckConfig(self.new_deck_config()?),
@ -702,11 +704,11 @@ impl Backend {
}) })
} }
fn add_or_update_deck_config(&self, conf_json: String) -> Result<i64> { fn add_or_update_deck_config(&self, input: AddOrUpdateDeckConfigIn) -> Result<i64> {
let mut conf: DeckConf = serde_json::from_str(&conf_json)?; let mut conf: DeckConf = serde_json::from_str(&input.config)?;
self.with_col(|col| { self.with_col(|col| {
col.transact(None, |col| { col.transact(None, |col| {
col.add_or_update_deck_config(&mut conf)?; col.add_or_update_deck_config(&mut conf, input.preserve_usn_and_mtime)?;
Ok(conf.id.0) Ok(conf.id.0)
}) })
}) })

View file

@ -219,9 +219,15 @@ impl Collection {
} }
} }
pub(crate) fn add_or_update_deck_config(&self, conf: &mut DeckConf) -> Result<()> { pub(crate) fn add_or_update_deck_config(
conf.mtime = TimestampSecs::now(); &self,
conf.usn = self.usn()?; conf: &mut DeckConf,
preserve_usn_and_mtime: bool,
) -> Result<()> {
if !preserve_usn_and_mtime {
conf.mtime = TimestampSecs::now();
conf.usn = self.usn()?;
}
let orig = self.storage.get_deck_config(conf.id)?; let orig = self.storage.get_deck_config(conf.id)?;
if let Some(_orig) = orig { if let Some(_orig) = orig {
self.storage.update_deck_conf(&conf) self.storage.update_deck_conf(&conf)