move deck conf handling to backend

This commit is contained in:
Damien Elmes 2020-03-30 14:39:46 +10:00
parent 5b26b20697
commit 004cc2b5f8
15 changed files with 411 additions and 53 deletions

View file

@ -50,6 +50,11 @@ message BackendInput {
int64 get_card = 38;
Card update_card = 39;
Card add_card = 40;
int64 get_deck_config = 41;
string add_or_update_deck_config = 42;
Empty all_deck_config = 43;
Empty new_deck_config = 44;
int64 remove_deck_config = 45;
}
}
@ -83,6 +88,11 @@ message BackendOutput {
GetCardOut get_card = 38;
Empty update_card = 39;
int64 add_card = 40;
string get_deck_config = 41;
int64 add_or_update_deck_config = 42;
string all_deck_config = 43;
string new_deck_config = 44;
Empty remove_deck_config = 45;
BackendError error = 2047;
}

View file

@ -95,7 +95,6 @@ defaultConf = {
class DeckManager:
decks: Dict[str, Any]
dconf: Dict[str, Any]
# Registry save/load
#############################################################
@ -103,36 +102,27 @@ class DeckManager:
def __init__(self, col: anki.storage._Collection) -> None:
self.col = col.weakref()
self.decks = {}
self.dconf = {}
def load(self, decks: str, dconf: str) -> None:
self.decks = json.loads(decks)
self.dconf = json.loads(dconf)
# set limits to within bounds
found = False
for c in list(self.dconf.values()):
for t in ("rev", "new"):
pd = "perDay"
if c[t][pd] > 999999:
c[t][pd] = 999999
self.save(c)
found = True
if not found:
self.changed = False
self.changed = False
def save(self, g: Optional[Any] = None) -> None:
"Can be called with either a deck or a deck configuration."
if g:
g["mod"] = intTime()
g["usn"] = self.col.usn()
# deck conf?
if "maxTaken" in g:
self.updateConf(g)
return
else:
g["mod"] = intTime()
g["usn"] = self.col.usn()
self.changed = True
def flush(self) -> None:
if self.changed:
self.col.db.execute(
"update col set decks=?, dconf=?",
json.dumps(self.decks),
json.dumps(self.dconf),
"update col set decks=?", json.dumps(self.decks),
)
self.changed = False
@ -368,7 +358,7 @@ class DeckManager:
def allConf(self) -> List:
"A list of all deck config."
return list(self.dconf.values())
return list(self.col.backend.all_deck_config().values())
def confForDid(self, did: int) -> Any:
deck = self.get(did, default=False)
@ -381,32 +371,25 @@ class DeckManager:
return deck
def getConf(self, confId: int) -> Any:
return self.dconf[str(confId)]
return self.col.backend.get_deck_config(confId)
def updateConf(self, g: Dict[str, Any]) -> None:
self.dconf[str(g["id"])] = g
self.save()
self.col.backend.add_or_update_deck_config(g)
def confId(self, name: str, cloneFrom: Optional[Dict[str, Any]] = None) -> int:
"Create a new configuration and return id."
if cloneFrom is None:
cloneFrom = defaultConf
c = copy.deepcopy(cloneFrom)
while 1:
id = intTime(1000)
if str(id) not in self.dconf:
break
c["id"] = id
c["name"] = name
self.dconf[str(id)] = c
self.save(c)
return id
if cloneFrom is not None:
conf = copy.deepcopy(cloneFrom)
conf["id"] = 0
else:
conf = self.col.backend.new_deck_config()
conf["name"] = name
self.updateConf(conf)
return conf["id"]
def remConf(self, id) -> None:
"Remove a configuration and update all decks using it."
assert int(id) != 1
self.col.modSchema(check=True)
del self.dconf[str(id)]
for g in self.all():
# ignore cram decks
if "conf" not in g:
@ -414,6 +397,7 @@ class DeckManager:
if str(g["conf"]) == str(id):
g["conf"] = 1
self.save(g)
self.col.backend.remove_deck_config(id)
def setConf(self, grp: Dict[str, Any], id: int) -> None:
grp["conf"] = id
@ -428,11 +412,10 @@ class DeckManager:
def restoreToDefault(self, conf) -> None:
oldOrder = conf["new"]["order"]
new = copy.deepcopy(defaultConf)
new = self.col.backend.new_deck_config()
new["id"] = conf["id"]
new["name"] = conf["name"]
self.dconf[str(conf["id"])] = new
self.save(new)
self.updateConf(new)
# if it was previously randomized, resort
if not oldOrder:
self.col.sched.resortConf(new)

View file

@ -3,6 +3,7 @@
# pylint: skip-file
import enum
import json
import os
from dataclasses import dataclass
from typing import (
@ -43,7 +44,6 @@ try:
except:
# add compat layer for 32 bit builds that can't use orjson
print("reverting to stock json")
import json
class orjson: # type: ignore
def dumps(obj: Any) -> bytes:
@ -490,6 +490,32 @@ class RustBackend:
def add_card(self, card: BackendCard) -> None:
card.id = self._run_command(pb.BackendInput(add_card=card)).add_card
def get_deck_config(self, dcid: int) -> Dict[str, Any]:
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:
conf_json = json.dumps(conf)
id = self._run_command(
pb.BackendInput(add_or_update_deck_config=conf_json)
).add_or_update_deck_config
conf["id"] = id
def all_deck_config(self) -> Dict[int, Dict[str, Any]]:
jstr = self._run_command(
pb.BackendInput(all_deck_config=pb.Empty())
).all_deck_config
return json.loads(jstr)
def new_deck_config(self) -> Dict[str, Any]:
jstr = self._run_command(
pb.BackendInput(new_deck_config=pb.Empty())
).new_deck_config
return json.loads(jstr)
def remove_deck_config(self, dcid: int) -> None:
self._run_command(pb.BackendInput(remove_deck_config=dcid))
def translate_string_in(
key: TR, **kwargs: Union[str, int, float]

View file

@ -8,6 +8,7 @@ use crate::card::{Card, CardID};
use crate::card::{CardQueue, CardType};
use crate::collection::{open_collection, Collection};
use crate::config::SortKind;
use crate::deckconf::{DeckConf, DeckConfID};
use crate::decks::DeckID;
use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind};
use crate::i18n::{tr_args, FString, I18n};
@ -68,6 +69,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
AnkiError::Interrupted => V::Interrupted(Empty {}),
AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}),
AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}),
AnkiError::SchemaChange => V::InvalidInput(pb::Empty {}),
};
pb::BackendError {
@ -261,6 +263,16 @@ impl Backend {
OValue::UpdateCard(pb::Empty {})
}
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::AllDeckConfig(_) => OValue::AllDeckConfig(self.all_deck_config()?),
Value::NewDeckConfig(_) => OValue::NewDeckConfig(self.new_deck_config()?),
Value::RemoveDeckConfig(dcid) => {
self.remove_deck_config(dcid)?;
OValue::RemoveDeckConfig(pb::Empty {})
}
})
}
@ -651,6 +663,37 @@ impl Backend {
self.with_col(|col| col.transact(None, |ctx| ctx.add_card(&mut card)))?;
Ok(card.id.0)
}
fn get_deck_config(&self, dcid: i64) -> Result<String> {
self.with_col(|col| {
let conf = col.get_deck_config(DeckConfID(dcid), true)?.unwrap();
Ok(serde_json::to_string(&conf)?)
})
}
fn add_or_update_deck_config(&self, conf_json: String) -> Result<i64> {
let mut conf: DeckConf = serde_json::from_str(&conf_json)?;
self.with_col(|col| {
col.transact(None, |col| {
col.add_or_update_deck_config(&mut conf)?;
Ok(conf.id.0)
})
})
}
fn all_deck_config(&self) -> Result<String> {
self.with_col(|col| {
serde_json::to_string(&col.storage.all_deck_conf()?).map_err(Into::into)
})
}
fn new_deck_config(&self) -> Result<String> {
serde_json::to_string(&DeckConf::default()).map_err(Into::into)
}
fn remove_deck_config(&self, dcid: i64) -> Result<()> {
self.with_col(|col| col.transact(None, |col| col.remove_deck_config(DeckConfID(dcid))))
}
}
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {

View file

@ -95,7 +95,6 @@ impl Undoable for UpdateCardUndo {
.storage
.get_card(self.0.id)?
.ok_or_else(|| AnkiError::invalid_input("card disappeared"))?;
// when called here, update_card should be placing the original content into the redo queue
col.update_card(&mut self.0.clone(), &current)
}
}

View file

@ -144,4 +144,12 @@ impl Collection {
// if we cache this in the future, must make sure to invalidate cache when usn bumped in sync.finish()
self.storage.usn(self.server)
}
pub(crate) fn ensure_schema_modified(&self) -> Result<()> {
if !self.storage.schema_modified()? {
Err(AnkiError::SchemaChange)
} else {
Ok(())
}
}
}

View file

@ -2,19 +2,10 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::decks::DeckID;
use serde::Deserialize as DeTrait;
use crate::serde::default_on_invalid;
use serde_aux::field_attributes::deserialize_number_from_string;
use serde_derive::Deserialize;
use serde_json::Value;
pub(crate) fn default_on_invalid<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Default + DeTrait<'de>,
D: serde::de::Deserializer<'de>,
{
let v: Value = DeTrait::deserialize(deserializer)?;
Ok(T::deserialize(v).unwrap_or_default())
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {

243
rslib/src/deckconf.rs Normal file
View file

@ -0,0 +1,243 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
collection::Collection,
define_newtype,
err::{AnkiError, Result},
serde::default_on_invalid,
timestamp::{TimestampMillis, TimestampSecs},
types::Usn,
};
use serde_aux::field_attributes::{deserialize_bool_from_anything, deserialize_number_from_string};
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_tuple::Serialize_tuple;
use std::collections::HashMap;
define_newtype!(DeckConfID, i64);
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeckConf {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub(crate) id: DeckConfID,
#[serde(rename = "mod", deserialize_with = "deserialize_number_from_string")]
pub(crate) mtime: TimestampSecs,
pub(crate) name: String,
pub(crate) usn: Usn,
max_taken: i32,
autoplay: bool,
#[serde(deserialize_with = "deserialize_bool_from_anything")]
timer: bool,
#[serde(default)]
replayq: bool,
pub(crate) new: NewConf,
pub(crate) rev: RevConf,
pub(crate) lapse: LapseConf,
#[serde(flatten)]
other: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct NewConf {
#[serde(default)]
bury: bool,
#[serde(deserialize_with = "default_on_invalid")]
delays: Vec<f32>,
initial_factor: u16,
#[serde(deserialize_with = "default_on_invalid")]
ints: NewCardIntervals,
#[serde(deserialize_with = "default_on_invalid")]
order: NewCardOrder,
#[serde(deserialize_with = "default_on_invalid")]
pub(crate) per_day: u32,
#[serde(flatten)]
other: HashMap<String, Value>,
}
#[derive(Serialize_tuple, Deserialize, Debug, PartialEq, Clone)]
pub struct NewCardIntervals {
good: u16,
easy: u16,
_unused: u16,
}
impl Default for NewCardIntervals {
fn default() -> Self {
Self {
good: 1,
easy: 4,
_unused: 7,
}
}
}
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)]
#[repr(u8)]
pub enum NewCardOrder {
Random = 0,
Due = 1,
}
impl Default for NewCardOrder {
fn default() -> Self {
Self::Due
}
}
fn hard_factor_default() -> f32 {
1.2
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RevConf {
#[serde(default)]
bury: bool,
ease4: f32,
ivl_fct: f32,
max_ivl: u32,
#[serde(deserialize_with = "default_on_invalid")]
pub(crate) per_day: u32,
#[serde(default = "hard_factor_default")]
hard_factor: f32,
#[serde(flatten)]
other: HashMap<String, Value>,
}
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)]
#[repr(u8)]
pub enum LeechAction {
Suspend = 0,
TagOnly = 1,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LapseConf {
#[serde(deserialize_with = "default_on_invalid")]
delays: Vec<f32>,
#[serde(deserialize_with = "default_on_invalid")]
leech_action: LeechAction,
leech_fails: u32,
min_int: u32,
mult: f32,
#[serde(flatten)]
other: HashMap<String, Value>,
}
impl Default for LeechAction {
fn default() -> Self {
LeechAction::Suspend
}
}
impl Default for RevConf {
fn default() -> Self {
RevConf {
bury: false,
ease4: 1.3,
ivl_fct: 1.0,
max_ivl: 36500,
per_day: 200,
hard_factor: 1.2,
other: Default::default(),
}
}
}
impl Default for NewConf {
fn default() -> Self {
NewConf {
bury: false,
delays: vec![1.0, 10.0],
initial_factor: 2500,
ints: NewCardIntervals::default(),
order: NewCardOrder::default(),
per_day: 20,
other: Default::default(),
}
}
}
impl Default for LapseConf {
fn default() -> Self {
LapseConf {
delays: vec![10.0],
leech_action: LeechAction::default(),
leech_fails: 8,
min_int: 1,
mult: 0.0,
other: Default::default(),
}
}
}
impl Default for DeckConf {
fn default() -> Self {
DeckConf {
id: DeckConfID(0),
mtime: TimestampSecs(0),
name: "Default".to_string(),
usn: Usn(0),
max_taken: 60,
autoplay: true,
timer: false,
replayq: true,
new: Default::default(),
rev: Default::default(),
lapse: Default::default(),
other: Default::default(),
}
}
}
impl Collection {
pub fn get_deck_config(&self, dcid: DeckConfID, fallback: bool) -> Result<Option<DeckConf>> {
let conf = self.storage.all_deck_conf()?;
if let Some(conf) = conf.get(&dcid) {
return Ok(Some(conf.clone()));
}
if fallback {
if let Some(conf) = conf.get(&DeckConfID(1)) {
return Ok(Some(conf.clone()));
}
// if even the default deck config is missing, just return the defaults
return Ok(Some(DeckConf::default()));
}
Ok(None)
}
pub(crate) fn add_or_update_deck_config(&self, conf: &mut DeckConf) -> Result<()> {
let mut allconf = self.storage.all_deck_conf()?;
if conf.id.0 == 0 {
conf.id.0 = TimestampMillis::now().0;
loop {
if !allconf.contains_key(&conf.id) {
break;
}
conf.id.0 += 1;
}
}
conf.mtime = TimestampSecs::now();
conf.usn = self.usn()?;
allconf.insert(conf.id, conf.clone());
self.storage.flush_deck_conf(&allconf)
}
pub(crate) fn remove_deck_config(&self, dcid: DeckConfID) -> Result<()> {
if dcid.0 == 1 {
return Err(AnkiError::invalid_input("can't delete default conf"));
}
self.ensure_schema_modified()?;
let mut allconf = self.storage.all_deck_conf()?;
allconf.remove(&dcid);
self.storage.flush_deck_conf(&allconf)
}
}

View file

@ -6,7 +6,6 @@ use serde_aux::field_attributes::deserialize_number_from_string;
use serde_derive::Deserialize;
define_newtype!(DeckID, i64);
define_newtype!(DeckConfID, i64);
#[derive(Deserialize)]
pub struct Deck {

View file

@ -39,6 +39,9 @@ pub enum AnkiError {
#[fail(display = "Close the existing collection first.")]
CollectionAlreadyOpen,
#[fail(display = "Operation modifies schema, but schema not marked modified.")]
SchemaChange,
}
// error helpers

View file

@ -14,6 +14,7 @@ pub mod card;
pub mod cloze;
pub mod collection;
pub mod config;
pub mod deckconf;
pub mod decks;
pub mod err;
pub mod i18n;
@ -24,6 +25,7 @@ pub mod notes;
pub mod notetypes;
pub mod sched;
pub mod search;
pub mod serde;
pub mod storage;
pub mod template;
pub mod template_filters;

14
rslib/src/serde.rs Normal file
View file

@ -0,0 +1,14 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use serde::Deserialize as DeTrait;
use serde_json::Value;
pub(crate) fn default_on_invalid<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Default + DeTrait<'de>,
D: serde::de::Deserializer<'de>,
{
let v: Value = DeTrait::deserialize(deserializer)?;
Ok(T::deserialize(v).unwrap_or_default())
}

View file

@ -0,0 +1,29 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::SqliteStorage;
use crate::{
deckconf::{DeckConf, DeckConfID},
err::{AnkiError, Result},
};
use rusqlite::{params, NO_PARAMS};
use std::collections::HashMap;
impl SqliteStorage {
pub(crate) fn all_deck_conf(&self) -> Result<HashMap<DeckConfID, DeckConf>> {
self.db
.prepare_cached("select dconf from col")?
.query_and_then(NO_PARAMS, |row| -> Result<_> {
Ok(serde_json::from_str(row.get_raw(0).as_str()?)?)
})?
.next()
.ok_or_else(|| AnkiError::invalid_input("no col table"))?
}
pub(crate) fn flush_deck_conf(&self, conf: &HashMap<DeckConfID, DeckConf>) -> Result<()> {
self.db
.prepare_cached("update col set dconf = ?")?
.execute(params![&serde_json::to_string(conf)?])?;
Ok(())
}
}

View file

@ -1,4 +1,5 @@
mod card;
mod deckconf;
mod sqlite;
pub(crate) use sqlite::SqliteStorage;

View file

@ -304,4 +304,11 @@ impl SqliteStorage {
conf.rollover,
))
}
pub(crate) fn schema_modified(&self) -> Result<bool> {
self.db
.prepare_cached("select scm > ls from col")?
.query_row(NO_PARAMS, |row| row.get(0))
.map_err(Into::into)
}
}