From 333d0735ffbc160d629abe5ae4a5fdfd5a1300ac Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 3 Apr 2020 13:54:52 +1000 Subject: [PATCH] preserve mtime/usn when syncing deck config, and add snake_case names --- proto/backend.proto | 7 ++++- pylib/anki/decks.py | 53 ++++++++++++++++++++++------------- pylib/anki/exporting.py | 2 +- pylib/anki/importing/anki2.py | 4 +-- pylib/anki/rsbackend.py | 17 +++++++++-- pylib/anki/sync.py | 7 +++-- pylib/tests/test_exporting.py | 4 +-- pylib/tests/test_schedv1.py | 2 +- pylib/tests/test_schedv2.py | 10 +++---- qt/aqt/customstudy.py | 2 +- qt/aqt/deckconf.py | 4 +-- rslib/src/backend/mod.rs | 14 +++++---- rslib/src/deckconf.rs | 12 ++++++-- 13 files changed, 90 insertions(+), 48 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index 4d387ec02..5a141f780 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -51,7 +51,7 @@ message BackendInput { Card update_card = 39; Card add_card = 40; 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 new_deck_config = 44; int64 remove_deck_config = 45; @@ -414,3 +414,8 @@ message Card { message CloseCollectionIn { bool downgrade_to_schema11 = 1; } + +message AddOrUpdateDeckConfigIn { + string config = 1; + bool preserve_usn_and_mtime = 2; +} diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 1a64a833d..8c617ed59 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -74,7 +74,7 @@ class DeckManager: if g: # deck conf? if "maxTaken" in g: - self.updateConf(g) + self.update_config(g) return else: g["mod"] = intTime() @@ -318,7 +318,7 @@ class DeckManager: # Deck configurations ############################################################# - def allConf(self) -> List: + def all_config(self) -> List: "A list of all deck config." return list(self.col.backend.all_deck_config()) @@ -327,32 +327,38 @@ class DeckManager: assert deck if "conf" in deck: dcid = int(deck["conf"]) # may be a string - conf = self.getConf(dcid) + conf = self.get_config(dcid) conf["dyn"] = False return conf # dynamic decks have embedded conf return deck - def getConf(self, confId: int) -> Any: + def get_config(self, conf_id: int) -> Any: if self._dconf_cache is not None: - return self._dconf_cache.get(confId) - return self.col.backend.get_deck_config(confId) + return self._dconf_cache.get(conf_id) + return self.col.backend.get_deck_config(conf_id) - def updateConf(self, g: Dict[str, Any]) -> None: - self.col.backend.add_or_update_deck_config(g) + def update_config(self, conf: Dict[str, Any], preserve_usn=False) -> None: + self.col.backend.add_or_update_deck_config(conf, preserve_usn) - def confId(self, name: str, cloneFrom: Optional[Dict[str, Any]] = None) -> int: - "Create a new configuration and return id." - if cloneFrom is not None: - conf = copy.deepcopy(cloneFrom) + def add_config( + self, name: str, clone_from: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + if clone_from is not None: + conf = copy.deepcopy(clone_from) conf["id"] = 0 else: conf = self.col.backend.new_deck_config() conf["name"] = name - self.updateConf(conf) - return conf["id"] + self.update_config(conf) + 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." self.col.modSchema(check=True) for g in self.all(): @@ -380,14 +386,21 @@ class DeckManager: new = self.col.backend.new_deck_config() new["id"] = conf["id"] new["name"] = conf["name"] - self.updateConf(new) + self.update_config(new) # if it was previously randomized, re-sort if not oldOrder: 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 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): self._dconf_cache = None @@ -613,8 +626,10 @@ class DeckManager: def beforeUpload(self) -> None: for d in self.all(): d["usn"] = 0 - for c in self.allConf(): - c["usn"] = 0 + for c in self.all_config(): + if c["usn"] != 0: + c["usn"] = 0 + self.update_config(c, preserve_usn=True) self.save() # Dynamic decks diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 92a77d2a9..cba68560d 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -249,7 +249,7 @@ class AnkiExporter(Exporter): # copy used deck confs for dc in self.src.decks.allConf(): if dc["id"] in dconfs: - self.dst.decks.updateConf(dc) + self.dst.decks.update_config(dc) # find used media media = {} self.mediaDir = self.src.media.dir() diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 342877206..8a4d308aa 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -277,9 +277,9 @@ class Anki2Importer(Importer): newid = self.dst.decks.id(name) # pull conf over 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.updateConf(conf) + self.dst.decks.update_config(conf) g2 = self.dst.decks.get(newid) g2["conf"] = g["conf"] self.dst.decks.save(g2) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index dd4778d8f..55b3a9f6c 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -2,6 +2,15 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # 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 json import os @@ -502,10 +511,14 @@ class RustBackend: jstr = self._run_command(pb.BackendInput(get_deck_config=dcid)).get_deck_config 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) 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 conf["id"] = id diff --git a/pylib/anki/sync.py b/pylib/anki/sync.py index 84f0a9e9f..7943f616d 100644 --- a/pylib/anki/sync.py +++ b/pylib/anki/sync.py @@ -374,9 +374,10 @@ from notes where %s""" decks = [g for g in self.col.decks.all() if g["usn"] == -1] for g in decks: 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: g["usn"] = self.maxUsn + self.col.decks.update_config(g, preserve_usn=True) self.col.decks.save() return [decks, dconf] @@ -392,12 +393,12 @@ from notes where %s""" self.col.decks.update(r) for r in rchg[1]: try: - l = self.col.decks.getConf(r["id"]) + l = self.col.decks.get_config(r["id"]) except KeyError: l = None # if missing locally or server is newer, update if not l or r["mod"] > l["mod"]: - self.col.decks.updateConf(r) + self.col.decks.update_config(r) # Tags ########################################################################## diff --git a/pylib/tests/test_exporting.py b/pylib/tests/test_exporting.py index db29d018e..e2afcb8c7 100644 --- a/pylib/tests/test_exporting.py +++ b/pylib/tests/test_exporting.py @@ -45,8 +45,8 @@ def test_export_anki(): # create a new deck with its own conf to test conf copying did = deck.decks.id("test") dobj = deck.decks.get(did) - confId = deck.decks.confId("newconf") - conf = deck.decks.getConf(confId) + confId = deck.decks.add_config_returning_id("newconf") + conf = deck.decks.get_config(confId) conf["new"]["perDay"] = 5 deck.decks.save(conf) deck.decks.setConf(dobj, confId) diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index f3324ac91..d26ef9ca3 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -90,7 +90,7 @@ def test_newLimits(): f.model()["did"] = g2 d.addNote(f) # 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.reset() # both confs have defaulted to a limit of 20 diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index f18e00e8e..4e1d822d3 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -90,7 +90,7 @@ def test_newLimits(): f.model()["did"] = g2 d.addNote(f) # 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.reset() # 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")) child = d.decks.get(d.decks.id("parent::child")) - pconf = d.decks.getConf(d.decks.confId("parentConf")) - cconf = d.decks.getConf(d.decks.confId("childConf")) + pconf = d.decks.get_config(d.decks.add_config_returning_id("parentConf")) + cconf = d.decks.get_config(d.decks.add_config_returning_id("childConf")) pconf["rev"]["perDay"] = 5 - d.decks.updateConf(pconf) + d.decks.update_config(pconf) d.decks.setConf(parent, pconf["id"]) cconf["rev"]["perDay"] = 10 - d.decks.updateConf(cconf) + d.decks.update_config(cconf) d.decks.setConf(child, cconf["id"]) m = d.models.current() diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index ffe8571c1..dd8b5b51b 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -26,7 +26,7 @@ class CustomStudy(QDialog): QDialog.__init__(self, mw) self.mw = mw 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.created_custom_study = False f.setupUi(self) diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index 6527ff22b..b0c4e21ea 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -123,7 +123,7 @@ class DeckConf(QDialog): # first, save currently entered data to current conf self.saveConf() # 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 self.deck["conf"] = id # then reload the conf list @@ -133,7 +133,7 @@ class DeckConf(QDialog): if int(self.conf["id"]) == 1: showInfo(_("The default configuration can't be removed."), self) else: - self.mw.col.decks.remConf(self.conf["id"]) + self.mw.col.decks.remove_config(self.conf["id"]) self.deck["conf"] = 1 self.loadConfs() diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 9e8979413..02597c413 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -3,7 +3,9 @@ use crate::backend::dbproxy::db_command_bytes; 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::{CardQueue, CardType}; use crate::collection::{open_collection, Collection}; @@ -268,8 +270,8 @@ impl Backend { } Value::AddCard(card) => OValue::AddCard(self.add_card(card)?), Value::GetDeckConfig(dcid) => OValue::GetDeckConfig(self.get_deck_config(dcid)?), - Value::AddOrUpdateDeckConfig(conf_json) => { - OValue::AddOrUpdateDeckConfig(self.add_or_update_deck_config(conf_json)?) + Value::AddOrUpdateDeckConfig(input) => { + OValue::AddOrUpdateDeckConfig(self.add_or_update_deck_config(input)?) } Value::AllDeckConfig(_) => OValue::AllDeckConfig(self.all_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 { - let mut conf: DeckConf = serde_json::from_str(&conf_json)?; + fn add_or_update_deck_config(&self, input: AddOrUpdateDeckConfigIn) -> Result { + let mut conf: DeckConf = serde_json::from_str(&input.config)?; self.with_col(|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) }) }) diff --git a/rslib/src/deckconf.rs b/rslib/src/deckconf.rs index f73fa124c..a189d35b5 100644 --- a/rslib/src/deckconf.rs +++ b/rslib/src/deckconf.rs @@ -219,9 +219,15 @@ impl Collection { } } - pub(crate) fn add_or_update_deck_config(&self, conf: &mut DeckConf) -> Result<()> { - conf.mtime = TimestampSecs::now(); - conf.usn = self.usn()?; + pub(crate) fn add_or_update_deck_config( + &self, + 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)?; if let Some(_orig) = orig { self.storage.update_deck_conf(&conf)