add stock note types in backend

This commit is contained in:
Damien Elmes 2020-04-13 19:14:30 +10:00
parent 7811a04df8
commit 540892639f
10 changed files with 342 additions and 42 deletions

View file

@ -533,8 +533,13 @@ message NoteTypeConfig {
}
message CardRequirement {
enum CardRequirementKind {
None = 0;
Any = 1;
All = 2;
}
uint32 card_ord = 1;
uint32 kind = 2;
CardRequirementKind kind = 2;
repeated uint32 field_ords = 3;
}
@ -547,3 +552,11 @@ message NoteType {
repeated NoteField fields = 8;
repeated CardTemplate templates = 9;
}
enum StockNoteType {
StockNoteTypeBasic = 0;
StockNoteTypeBasicAndReversed = 1;
StockNoteTypeBasicOptionalReversed = 2;
StockNoteTypeBasicTyping = 3;
StockNoteTypeCloze = 4;
}

View file

@ -13,13 +13,6 @@ from anki.dbproxy import DBProxy
from anki.lang import _
from anki.media import media_paths_from_col_path
from anki.rsbackend import RustBackend
from anki.stdmodels import (
addBasicModel,
addBasicTypingModel,
addClozeModel,
addForwardOptionalReverse,
addForwardReverse,
)
from anki.utils import intTime
@ -51,19 +44,13 @@ def Collection(
db = DBProxy(weakref.proxy(backend), path)
# initial setup required?
create = db.scalar("select models = '{}' from col")
create = db.scalar("select decks = '{}' from col")
if create:
initial_db_setup(db)
# add db to col and do any remaining upgrades
col = _Collection(db, backend=backend, server=server, log=should_log)
if create:
# add in reverse order so basic is default
addClozeModel(col)
addBasicTypingModel(col)
addForwardOptionalReverse(col)
addForwardReverse(col)
addBasicModel(col)
col.save()
else:
db.begin()

View file

@ -827,7 +827,7 @@ impl Backend {
pb::set_config_json::Op::Val(val) => {
// ensure it's a well-formed object
let val: JsonValue = serde_json::from_slice(&val)?;
col.set_config(&input.key, &val)
col.set_config(input.key.as_str(), &val)
}
pb::set_config_json::Op::Remove(_) => col.remove_config(&input.key),
}

View file

@ -1,10 +1,10 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::collection::Collection;
use crate::decks::DeckID;
use crate::err::Result;
use crate::timestamp::TimestampSecs;
use crate::{
collection::Collection, decks::DeckID, err::Result, notetype::NoteTypeID,
timestamp::TimestampSecs,
};
use serde::{de::DeserializeOwned, Serialize};
use serde_aux::field_attributes::deserialize_bool_from_anything;
use serde_derive::Deserialize;
@ -38,6 +38,7 @@ pub(crate) enum ConfigKey {
CreationOffset,
Rollover,
LocalOffset,
CurrentNoteTypeID,
}
impl From<ConfigKey> for &'static str {
@ -49,6 +50,7 @@ impl From<ConfigKey> for &'static str {
ConfigKey::CreationOffset => "creationOffset",
ConfigKey::Rollover => "rollover",
ConfigKey::LocalOffset => "localOffset",
ConfigKey::CurrentNoteTypeID => "curModel",
}
}
}
@ -83,9 +85,12 @@ impl Collection {
self.get_config_optional(key).unwrap_or_default()
}
pub(crate) fn set_config<T: Serialize>(&self, key: &str, val: &T) -> Result<()> {
pub(crate) fn set_config<'a, T: Serialize, K>(&self, key: K, val: &T) -> Result<()>
where
K: Into<&'a str>,
{
self.storage
.set_config_value(key, val, self.usn()?, TimestampSecs::now())
.set_config_value(key.into(), val, self.usn()?, TimestampSecs::now())
}
pub(crate) fn remove_config(&self, key: &str) -> Result<()> {
@ -117,6 +122,16 @@ impl Collection {
pub(crate) fn get_rollover(&self) -> Option<i8> {
self.get_config_optional(ConfigKey::Rollover)
}
#[allow(dead_code)]
pub(crate) fn get_current_notetype_id(&self) -> Option<NoteTypeID> {
self.get_config_optional(ConfigKey::CurrentNoteTypeID)
}
#[allow(dead_code)]
pub(crate) fn set_current_notetype_id(&self, id: NoteTypeID) -> Result<()> {
self.set_config(ConfigKey::CurrentNoteTypeID, &id)
}
}
#[derive(Deserialize, PartialEq, Debug)]

View file

@ -2,15 +2,20 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod schema11;
mod stock;
pub use crate::backend_proto::{
CardRequirement, CardTemplate, CardTemplateConfig, NoteField, NoteFieldConfig, NoteType,
NoteTypeConfig, NoteTypeKind,
card_requirement::CardRequirementKind, CardRequirement, CardTemplate, CardTemplateConfig,
NoteField, NoteFieldConfig, NoteType, NoteTypeConfig, NoteTypeKind,
};
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11};
use crate::{define_newtype, text::ensure_string_in_nfc};
use std::collections::HashSet;
use crate::{
define_newtype,
template::{without_legacy_template_directives, FieldRequirements, ParsedTemplate},
text::ensure_string_in_nfc,
};
use std::collections::{HashMap, HashSet};
use unicase::UniCase;
define_newtype!(NoteTypeID, i64);
@ -36,9 +41,6 @@ pub(crate) const DEFAULT_LATEX_HEADER: &str = r#"\documentclass[12pt]{article}
pub(crate) const DEFAULT_LATEX_FOOTER: &str = r#"\end{document}"#;
// other: vec![], // fixme: ensure empty map converted to empty bytes
// fixme: rollback savepoint when tags not changed
impl NoteType {
pub fn new() -> Self {
let mut nt = Self::default();
@ -87,6 +89,53 @@ impl NoteType {
}
}
fn update_requirements(&mut self) {
let field_map: HashMap<&str, u16> = self
.fields
.iter()
.enumerate()
.map(|(idx, field)| (field.name.as_str(), idx as u16))
.collect();
let reqs: Vec<_> = self
.templates
.iter()
.enumerate()
.map(|(ord, tmpl)| {
let conf = tmpl.config.as_ref().unwrap();
let normalized = without_legacy_template_directives(&conf.q_format);
if let Ok(tmpl) = ParsedTemplate::from_text(normalized.as_ref()) {
let mut req = match tmpl.requirements(&field_map) {
FieldRequirements::Any(ords) => CardRequirement {
card_ord: ord as u32,
kind: CardRequirementKind::Any as i32,
field_ords: ords.into_iter().map(|n| n as u32).collect(),
},
FieldRequirements::All(ords) => CardRequirement {
card_ord: ord as u32,
kind: CardRequirementKind::All as i32,
field_ords: ords.into_iter().map(|n| n as u32).collect(),
},
FieldRequirements::None => CardRequirement {
card_ord: ord as u32,
kind: CardRequirementKind::None as i32,
field_ords: vec![],
},
};
req.field_ords.sort_unstable();
req
} else {
// template parsing failures make card unsatisfiable
CardRequirement {
card_ord: ord as u32,
kind: CardRequirementKind::None as i32,
field_ords: vec![],
}
}
})
.collect();
self.config.as_mut().unwrap().reqs = reqs;
}
pub(crate) fn normalize_names(&mut self) {
ensure_string_in_nfc(&mut self.name);
for f in &mut self.fields {
@ -96,4 +145,37 @@ impl NoteType {
ensure_string_in_nfc(&mut t.name);
}
}
pub(crate) fn add_field<S: Into<String>>(&mut self, name: S) {
let mut config = NoteFieldConfig::default();
config.font_name = "Arial".to_string();
config.font_size = 20;
let mut field = NoteField::default();
field.name = name.into();
field.config = Some(config);
self.fields.push(field);
}
pub(crate) fn add_template<S1, S2, S3>(&mut self, name: S1, qfmt: S2, afmt: S3)
where
S1: Into<String>,
S2: Into<String>,
S3: Into<String>,
{
let mut config = CardTemplateConfig::default();
config.q_format = qfmt.into();
config.a_format = afmt.into();
let mut tmpl = CardTemplate::default();
tmpl.name = name.into();
tmpl.config = Some(config);
self.templates.push(tmpl);
}
pub(crate) fn prepare_for_adding(&mut self) {
self.normalize_names();
self.ensure_names_unique();
self.update_requirements();
}
}

View file

@ -16,7 +16,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_tuple::Serialize_tuple;
use std::collections::HashMap;
use super::NoteTypeID;
use super::{CardRequirementKind, NoteTypeID};
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone)]
#[repr(u8)]
@ -101,7 +101,7 @@ impl From<NoteTypeSchema11> for NoteType {
latex_post: nt.latex_post,
latex_svg: nt.latexsvg,
reqs: nt.req.0.into_iter().map(Into::into).collect(),
other: serde_json::to_vec(&nt.other).unwrap(),
other: other_to_bytes(&nt.other),
}),
fields: nt.flds.into_iter().map(Into::into).collect(),
templates: nt.tmpls.into_iter().map(Into::into).collect(),
@ -109,6 +109,29 @@ impl From<NoteTypeSchema11> for NoteType {
}
}
fn other_to_bytes(other: &HashMap<String, Value>) -> Vec<u8> {
if other.is_empty() {
vec![]
} else {
serde_json::to_vec(other).unwrap_or_else(|e| {
// theoretically should never happen
println!("serialization failed for {:?}: {}", other, e);
vec![]
})
}
}
fn bytes_to_other(bytes: &[u8]) -> HashMap<String, Value> {
if bytes.is_empty() {
Default::default()
} else {
serde_json::from_slice(bytes).unwrap_or_else(|e| {
println!("deserialization failed for other: {}", e);
Default::default()
})
}
}
impl From<NoteType> for NoteTypeSchema11 {
fn from(p: NoteType) -> Self {
let c = p.config.unwrap();
@ -135,7 +158,7 @@ impl From<NoteType> for NoteTypeSchema11 {
latex_post: c.latex_post,
latexsvg: c.latex_svg,
req: CardRequirementsSchema11(c.reqs.into_iter().map(Into::into).collect()),
other: serde_json::from_slice(&c.other).unwrap_or_default(),
other: bytes_to_other(&c.other),
}
}
}
@ -144,7 +167,11 @@ impl From<CardRequirementSchema11> for CardRequirement {
fn from(r: CardRequirementSchema11) -> Self {
CardRequirement {
card_ord: r.card_ord as u32,
kind: r.kind as u32,
kind: match r.kind {
FieldRequirementKindSchema11::Any => CardRequirementKind::Any,
FieldRequirementKindSchema11::All => CardRequirementKind::All,
FieldRequirementKindSchema11::None => CardRequirementKind::None,
} as i32,
field_ords: r.field_ords.into_iter().map(|n| n as u32).collect(),
}
}
@ -154,10 +181,10 @@ impl From<CardRequirement> for CardRequirementSchema11 {
fn from(p: CardRequirement) -> Self {
CardRequirementSchema11 {
card_ord: p.card_ord as u16,
kind: match p.kind {
0 => FieldRequirementKindSchema11::Any,
1 => FieldRequirementKindSchema11::All,
_ => FieldRequirementKindSchema11::None,
kind: match p.kind() {
CardRequirementKind::Any => FieldRequirementKindSchema11::Any,
CardRequirementKind::All => FieldRequirementKindSchema11::All,
CardRequirementKind::None => FieldRequirementKindSchema11::None,
},
field_ords: p.field_ords.into_iter().map(|n| n as u16).collect(),
}
@ -202,7 +229,7 @@ impl From<NoteFieldSchema11> for NoteField {
rtl: f.rtl,
font_name: f.font,
font_size: f.size as u32,
other: serde_json::to_vec(&f.other).unwrap(),
other: other_to_bytes(&f.other),
}),
}
}
@ -218,7 +245,7 @@ impl From<NoteField> for NoteFieldSchema11 {
rtl: conf.rtl,
font: conf.font_name,
size: conf.font_size as u16,
other: serde_json::from_slice(&conf.other).unwrap(),
other: bytes_to_other(&conf.other),
}
}
}
@ -259,7 +286,7 @@ impl From<CardTemplateSchema11> for CardTemplate {
target_deck_id: t.did.unwrap_or(DeckID(0)).0,
browser_font_name: t.bfont,
browser_font_size: t.bsize as u32,
other: serde_json::to_vec(&t.other).unwrap(),
other: other_to_bytes(&t.other),
}),
}
}
@ -282,7 +309,7 @@ impl From<CardTemplate> for CardTemplateSchema11 {
},
bfont: conf.browser_font_name,
bsize: conf.browser_font_size as u8,
other: serde_json::from_slice(&conf.other).unwrap(),
other: bytes_to_other(&conf.other),
}
}
}

130
rslib/src/notetype/stock.rs Normal file
View file

@ -0,0 +1,130 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::NoteTypeKind;
use crate::{
config::ConfigKey, err::Result, i18n::I18n, i18n::TR, notetype::NoteType,
storage::SqliteStorage, timestamp::TimestampSecs,
};
pub use crate::backend_proto::StockNoteType;
impl SqliteStorage {
pub(crate) fn add_stock_notetypes(&self, i18n: &I18n) -> Result<()> {
for (idx, mut nt) in all_stock_notetypes(i18n).into_iter().enumerate() {
self.add_new_notetype(&mut nt)?;
if idx == StockNoteType::Basic as usize {
self.set_config_value(
ConfigKey::CurrentNoteTypeID.into(),
&nt.id(),
self.usn(false)?,
TimestampSecs::now(),
)?;
}
}
Ok(())
}
}
// if changing this, make sure to update StockNoteType enum
pub fn all_stock_notetypes(i18n: &I18n) -> Vec<NoteType> {
vec![
basic(i18n),
basic_forward_reverse(i18n),
basic_optional_reverse(i18n),
basic_typing(i18n),
cloze(i18n),
]
}
/// returns {{name}}
fn fieldref<S: AsRef<str>>(name: S) -> String {
format!("{{{{{}}}}}", name.as_ref())
}
pub(crate) fn basic(i18n: &I18n) -> NoteType {
let mut nt = NoteType::new();
nt.name = i18n.tr(TR::NotetypesBasicName).into();
let front = i18n.tr(TR::NotetypesFrontField);
let back = i18n.tr(TR::NotetypesBackField);
nt.add_field(front.as_ref());
nt.add_field(back.as_ref());
nt.add_template(
i18n.tr(TR::NotetypesCard1Name),
fieldref(front),
format!(
"{}\n\n<hr id=answer>\n\n{}",
fieldref("FrontSide"),
fieldref(back),
),
);
nt.prepare_for_adding();
nt
}
pub(crate) fn basic_typing(i18n: &I18n) -> NoteType {
let mut nt = basic(i18n);
nt.name = i18n.tr(TR::NotetypesBasicTypeAnswerName).into();
let front = i18n.tr(TR::NotetypesFrontField);
let back = i18n.tr(TR::NotetypesBackField);
let tmpl = nt.templates[0].config.as_mut().unwrap();
tmpl.q_format = format!("{}\n\n{{{{type:{}}}}}", fieldref(front.as_ref()), back);
tmpl.a_format = format!(
"{}\n\n<hr id=answer>\n\n{{{{type:{}}}}}",
fieldref(front),
back
);
nt.prepare_for_adding();
nt
}
pub(crate) fn basic_forward_reverse(i18n: &I18n) -> NoteType {
let mut nt = basic(i18n);
nt.name = i18n.tr(TR::NotetypesBasicReversedName).into();
let front = i18n.tr(TR::NotetypesFrontField);
let back = i18n.tr(TR::NotetypesBackField);
nt.add_template(
i18n.tr(TR::NotetypesCard2Name),
fieldref(back),
format!(
"{}\n\n<hr id=answer>\n\n{}",
fieldref("FrontSide"),
fieldref(front),
),
);
nt.prepare_for_adding();
nt
}
pub(crate) fn basic_optional_reverse(i18n: &I18n) -> NoteType {
let mut nt = basic_forward_reverse(i18n);
nt.name = i18n.tr(TR::NotetypesBasicOptionalReversedName).into();
let addrev = i18n.tr(TR::NotetypesAddReverseField);
nt.add_field(addrev.as_ref());
let tmpl = nt.templates[1].config.as_mut().unwrap();
tmpl.q_format = format!("{{{{#{}}}}}{}{{{{/{}}}}}", addrev, tmpl.q_format, addrev);
nt.prepare_for_adding();
nt
}
pub(crate) fn cloze(i18n: &I18n) -> NoteType {
let mut nt = NoteType::new();
nt.name = i18n.tr(TR::NotetypesClozeName).into();
let text = i18n.tr(TR::NotetypesTextField);
nt.add_field(text.as_ref());
let fmt = format!("{{{{cloze:{}}}}}", text);
nt.add_template(nt.name.clone(), fmt.clone(), fmt);
let mut config = nt.config.as_mut().unwrap();
config.kind = NoteTypeKind::Cloze as i32;
config.css += "
.cloze {
font-weight: bold;
color: blue;
}
.nightMode .cloze {
color: lightblue;
}
";
nt.prepare_for_adding();
nt
}

View file

@ -1,3 +1,22 @@
insert into notetypes (id, name, mtime_secs, usn, config)
values
(?, ?, ?, ?, ?)
(
(
case
when ?1 in (
select
id
from notetypes
) then (
select
max(id) + 1
from notetypes
)
else ?1
end
),
?,
?,
?,
?
);

View file

@ -8,6 +8,7 @@ use crate::{
},
err::{AnkiError, DBErrorKind, Result},
notetype::{NoteTypeID, NoteTypeSchema11},
timestamp::TimestampMillis,
};
use prost::Message;
use rusqlite::{params, Row, NO_PARAMS};
@ -160,6 +161,27 @@ impl SqliteStorage {
Ok(())
}
pub(crate) fn add_new_notetype(&self, nt: &mut NoteType) -> Result<()> {
assert!(nt.id == 0);
let mut stmt = self.db.prepare_cached(include_str!("add_notetype.sql"))?;
let mut config_bytes = vec![];
nt.config.as_ref().unwrap().encode(&mut config_bytes)?;
stmt.execute(params![
TimestampMillis::now(),
nt.name,
nt.mtime_secs,
nt.usn,
config_bytes
])?;
nt.id = self.db.last_insert_rowid();
self.update_notetype_fields(nt.id(), &nt.fields)?;
self.update_notetype_templates(nt.id(), &nt.templates)?;
Ok(())
}
// Upgrading/downgrading
pub(crate) fn upgrade_notetypes_to_schema15(&self) -> Result<()> {
@ -181,6 +203,7 @@ impl SqliteStorage {
self.update_notetype_fields(ntid, &nt.fields)?;
self.update_notetype_templates(ntid, &nt.templates)?;
}
self.db.execute("update col set models = ''", NO_PARAMS)?;
Ok(())
}

View file

@ -20,6 +20,9 @@ fn unicase_compare(s1: &str, s2: &str) -> Ordering {
UniCase::new(s1).cmp(&UniCase::new(s2))
}
// fixme: rollback savepoint when tags not changed
// fixme: switch away from proto for top level struct
// currently public for dbproxy
#[derive(Debug)]
pub struct SqliteStorage {
@ -195,6 +198,7 @@ impl SqliteStorage {
if create {
storage.add_default_deck_config(i18n)?;
storage.add_stock_notetypes(i18n)?;
}
if create || upgrade {