mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
more work towards note adding
Still a prototype at this stage - we'll likely want a caching layer for note types, and I'm not sure of the merit of having fields in a separate table, since they're almost always required.
This commit is contained in:
parent
5d4f9dc3c0
commit
f75fd5335d
11 changed files with 247 additions and 27 deletions
|
@ -78,6 +78,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
|
||||||
AnkiError::SchemaChange => V::InvalidInput(pb::Empty {}),
|
AnkiError::SchemaChange => V::InvalidInput(pb::Empty {}),
|
||||||
AnkiError::JSONError { info } => V::JsonError(info),
|
AnkiError::JSONError { info } => V::JsonError(info),
|
||||||
AnkiError::ProtoError { info } => V::ProtoError(info),
|
AnkiError::ProtoError { info } => V::ProtoError(info),
|
||||||
|
AnkiError::NoCardsGenerated => todo!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
pb::BackendError {
|
pb::BackendError {
|
||||||
|
|
|
@ -99,6 +99,15 @@ impl Undoable for UpdateCardUndo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Card {
|
||||||
|
pub fn new(nid: NoteID, ord: u16, deck_id: DeckID) -> Self {
|
||||||
|
let mut card = Card::default();
|
||||||
|
card.nid = nid;
|
||||||
|
card.ord = ord;
|
||||||
|
card.did = deck_id;
|
||||||
|
card
|
||||||
|
}
|
||||||
|
}
|
||||||
impl Collection {
|
impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardID, func: F) -> Result<Card>
|
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardID, func: F) -> Result<Card>
|
||||||
|
@ -127,7 +136,6 @@ impl Collection {
|
||||||
self.storage.update_card(card)
|
self.storage.update_card(card)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn add_card(&mut self, card: &mut Card) -> Result<()> {
|
pub(crate) fn add_card(&mut self, card: &mut Card) -> Result<()> {
|
||||||
if card.id.0 != 0 {
|
if card.id.0 != 0 {
|
||||||
return Err(AnkiError::invalid_input("card id already set"));
|
return Err(AnkiError::invalid_input("card id already set"));
|
||||||
|
|
|
@ -8,7 +8,7 @@ use std::io;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AnkiError>;
|
pub type Result<T> = std::result::Result<T, AnkiError>;
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail, PartialEq)]
|
||||||
pub enum AnkiError {
|
pub enum AnkiError {
|
||||||
#[fail(display = "invalid input: {}", info)]
|
#[fail(display = "invalid input: {}", info)]
|
||||||
InvalidInput { info: String },
|
InvalidInput { info: String },
|
||||||
|
@ -48,6 +48,9 @@ pub enum AnkiError {
|
||||||
|
|
||||||
#[fail(display = "Operation modifies schema, but schema not marked modified.")]
|
#[fail(display = "Operation modifies schema, but schema not marked modified.")]
|
||||||
SchemaChange,
|
SchemaChange,
|
||||||
|
|
||||||
|
#[fail(display = "No cards generated.")]
|
||||||
|
NoCardsGenerated,
|
||||||
}
|
}
|
||||||
|
|
||||||
// error helpers
|
// error helpers
|
||||||
|
|
|
@ -404,7 +404,7 @@ where
|
||||||
&self.mgr.media_folder,
|
&self.mgr.media_folder,
|
||||||
)? {
|
)? {
|
||||||
// note was modified, needs saving
|
// note was modified, needs saving
|
||||||
note.prepare_for_update(nt.config.sort_field_idx as usize, usn);
|
note.prepare_for_update(nt, usn)?;
|
||||||
self.ctx.storage.update_note(¬e)?;
|
self.ctx.storage.update_note(¬e)?;
|
||||||
collection_modified = true;
|
collection_modified = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// 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
|
||||||
|
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::{
|
||||||
use crate::notetype::NoteTypeID;
|
card::Card,
|
||||||
use crate::text::strip_html_preserving_image_filenames;
|
collection::Collection,
|
||||||
use crate::timestamp::TimestampSecs;
|
define_newtype,
|
||||||
use crate::{collection::Collection, define_newtype, types::Usn};
|
err::{AnkiError, Result},
|
||||||
|
notetype::{CardGenContext, NoteField, NoteType, NoteTypeID},
|
||||||
|
text::strip_html_preserving_image_filenames,
|
||||||
|
timestamp::TimestampSecs,
|
||||||
|
types::Usn,
|
||||||
|
};
|
||||||
use num_integer::Integer;
|
use num_integer::Integer;
|
||||||
use std::{collections::HashSet, convert::TryInto};
|
use std::{collections::HashSet, convert::TryInto};
|
||||||
|
|
||||||
|
@ -27,15 +32,15 @@ pub struct Note {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Note {
|
impl Note {
|
||||||
pub(crate) fn new(ntid: NoteTypeID, field_count: usize) -> Self {
|
pub(crate) fn new(notetype: &NoteType) -> Self {
|
||||||
Note {
|
Note {
|
||||||
id: NoteID(0),
|
id: NoteID(0),
|
||||||
guid: guid(),
|
guid: guid(),
|
||||||
ntid,
|
ntid: notetype.id,
|
||||||
mtime: TimestampSecs(0),
|
mtime: TimestampSecs(0),
|
||||||
usn: Usn(0),
|
usn: Usn(0),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
fields: vec!["".to_string(); field_count],
|
fields: vec!["".to_string(); notetype.fields.len()],
|
||||||
sort_field: None,
|
sort_field: None,
|
||||||
checksum: None,
|
checksum: None,
|
||||||
}
|
}
|
||||||
|
@ -57,15 +62,24 @@ impl Note {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_for_update(&mut self, sort_field_idx: usize, usn: Usn) {
|
pub fn prepare_for_update(&mut self, nt: &NoteType, usn: Usn) -> Result<()> {
|
||||||
|
assert!(nt.id == self.ntid);
|
||||||
|
if nt.fields.len() != self.fields.len() {
|
||||||
|
return Err(AnkiError::invalid_input(format!(
|
||||||
|
"note has {} fields, expected {}",
|
||||||
|
self.fields.len(),
|
||||||
|
nt.fields.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
let field1_nohtml = strip_html_preserving_image_filenames(&self.fields()[0]);
|
let field1_nohtml = strip_html_preserving_image_filenames(&self.fields()[0]);
|
||||||
let checksum = field_checksum(field1_nohtml.as_ref());
|
let checksum = field_checksum(field1_nohtml.as_ref());
|
||||||
let sort_field = if sort_field_idx == 0 {
|
let sort_field = if nt.config.sort_field_idx == 0 {
|
||||||
field1_nohtml
|
field1_nohtml
|
||||||
} else {
|
} else {
|
||||||
strip_html_preserving_image_filenames(
|
strip_html_preserving_image_filenames(
|
||||||
self.fields
|
self.fields
|
||||||
.get(sort_field_idx)
|
.get(nt.config.sort_field_idx as usize)
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
.unwrap_or(""),
|
.unwrap_or(""),
|
||||||
)
|
)
|
||||||
|
@ -74,10 +88,10 @@ impl Note {
|
||||||
self.checksum = Some(checksum);
|
self.checksum = Some(checksum);
|
||||||
self.mtime = TimestampSecs::now();
|
self.mtime = TimestampSecs::now();
|
||||||
self.usn = usn;
|
self.usn = usn;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
pub(crate) fn nonempty_fields<'a>(&self, fields: &'a [NoteField]) -> HashSet<&'a str> {
|
||||||
pub(crate) fn nonempty_fields(&self) -> HashSet<u16> {
|
|
||||||
self.fields
|
self.fields
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
@ -85,7 +99,7 @@ impl Note {
|
||||||
if s.trim().is_empty() {
|
if s.trim().is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(ord as u16)
|
fields.get(ord).map(|f| f.name.as_str())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -119,17 +133,50 @@ fn anki_base91(mut n: u64) -> String {
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
|
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
|
||||||
self.transact(None, |col| {
|
self.transact(None, |col| {
|
||||||
println!("fixme: need to add cards, abort if no cards generated, etc");
|
let nt = col
|
||||||
// fixme: proper index
|
.storage
|
||||||
note.prepare_for_update(0, col.usn()?);
|
.get_full_notetype(note.ntid)?
|
||||||
col.storage.add_note(note)
|
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
||||||
|
|
||||||
|
let cardgen = CardGenContext::new(&nt, col.usn()?);
|
||||||
|
col.add_note_inner(note, &cardgen)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_note_inner(
|
||||||
|
&mut self,
|
||||||
|
note: &mut Note,
|
||||||
|
cardgen: &CardGenContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let nt = cardgen.notetype;
|
||||||
|
note.prepare_for_update(nt, cardgen.usn)?;
|
||||||
|
let nonempty_fields = note.nonempty_fields(&cardgen.notetype.fields);
|
||||||
|
let cards =
|
||||||
|
cardgen.new_cards_required(&nonempty_fields, nt.target_deck_id(), &Default::default());
|
||||||
|
if cards.is_empty() {
|
||||||
|
return Err(AnkiError::NoCardsGenerated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the note
|
||||||
|
self.storage.add_note(note)?;
|
||||||
|
// and its associated cards
|
||||||
|
for (card_ord, target_deck_id) in cards {
|
||||||
|
let mut card = Card::new(note.id, card_ord as u16, target_deck_id);
|
||||||
|
self.add_card(&mut card)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{anki_base91, field_checksum};
|
use super::{anki_base91, field_checksum};
|
||||||
|
use crate::{
|
||||||
|
collection::open_test_collection,
|
||||||
|
err::{AnkiError, Result},
|
||||||
|
search::SortMode,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_base91() {
|
fn test_base91() {
|
||||||
|
@ -145,4 +192,29 @@ mod test {
|
||||||
assert_eq!(field_checksum("test"), 2840236005);
|
assert_eq!(field_checksum("test"), 2840236005);
|
||||||
assert_eq!(field_checksum("今日"), 1464653051);
|
assert_eq!(field_checksum("今日"), 1464653051);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding() -> Result<()> {
|
||||||
|
let mut col = open_test_collection();
|
||||||
|
let nt = col.get_notetype_by_name("basic")?.unwrap();
|
||||||
|
|
||||||
|
let mut note = nt.new_note();
|
||||||
|
assert_eq!(col.add_note(&mut note), Err(AnkiError::NoCardsGenerated));
|
||||||
|
|
||||||
|
note.fields[1] = "foo".into();
|
||||||
|
assert_eq!(col.add_note(&mut note), Err(AnkiError::NoCardsGenerated));
|
||||||
|
|
||||||
|
note.fields[0] = "bar".into();
|
||||||
|
col.add_note(&mut note).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// fixme: add nt cache, refcount
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
85
rslib/src/notetype/cardgeninfo.rs
Normal file
85
rslib/src/notetype/cardgeninfo.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use super::NoteType;
|
||||||
|
use crate::{decks::DeckID, template::ParsedTemplate, types::Usn};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Info required to determine whether a particular card ordinal should exist,
|
||||||
|
/// and which deck it should be placed in.
|
||||||
|
pub(crate) struct SingleCardGenContext<'a> {
|
||||||
|
template: Option<ParsedTemplate<'a>>,
|
||||||
|
target_deck_id: Option<DeckID>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info required to determine which cards should be generated when note added/updated,
|
||||||
|
/// and where they should be placed.
|
||||||
|
pub(crate) struct CardGenContext<'a> {
|
||||||
|
pub usn: Usn,
|
||||||
|
pub notetype: &'a NoteType,
|
||||||
|
cards: Vec<SingleCardGenContext<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CardGenContext<'_> {
|
||||||
|
pub(crate) fn new(nt: &NoteType, usn: Usn) -> CardGenContext<'_> {
|
||||||
|
CardGenContext {
|
||||||
|
usn,
|
||||||
|
notetype: &nt,
|
||||||
|
cards: nt
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.map(|tmpl| SingleCardGenContext {
|
||||||
|
template: tmpl.parsed_question(),
|
||||||
|
target_deck_id: tmpl.target_deck_id(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If template[ord] generates a non-empty question given nonempty_fields, return the provided
|
||||||
|
/// deck id, or an overriden one. If question is empty, return None.
|
||||||
|
pub fn deck_id_if_nonempty(
|
||||||
|
&self,
|
||||||
|
card_ord: usize,
|
||||||
|
nonempty_fields: &HashSet<&str>,
|
||||||
|
target_deck_id: DeckID,
|
||||||
|
) -> Option<DeckID> {
|
||||||
|
let card = &self.cards[card_ord];
|
||||||
|
let template = match card.template {
|
||||||
|
Some(ref template) => template,
|
||||||
|
None => {
|
||||||
|
// template failed to parse; card can not be generated
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if template.renders_with_fields(&nonempty_fields) {
|
||||||
|
Some(card.target_deck_id.unwrap_or(target_deck_id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a list of (ordinal, deck id) for any new cards not in existing_ords
|
||||||
|
/// that are non-empty, and thus need to be added.
|
||||||
|
pub fn new_cards_required(
|
||||||
|
&self,
|
||||||
|
nonempty_fields: &HashSet<&str>,
|
||||||
|
target_deck_id: DeckID,
|
||||||
|
existing_ords: &HashSet<u16>,
|
||||||
|
) -> Vec<(usize, DeckID)> {
|
||||||
|
self.cards
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(ord, card)| {
|
||||||
|
let deck_id = card.target_deck_id.unwrap_or(target_deck_id);
|
||||||
|
if existing_ords.contains(&(ord as u16)) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.deck_id_if_nonempty(ord, nonempty_fields, deck_id)
|
||||||
|
.map(|did| (ord, did))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
use crate::backend_proto::{NoteField as NoteFieldProto, NoteFieldConfig, OptionalUInt32};
|
use crate::backend_proto::{NoteField as NoteFieldProto, NoteFieldConfig, OptionalUInt32};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct NoteField {
|
pub struct NoteField {
|
||||||
pub ord: Option<u32>,
|
pub ord: Option<u32>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// 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
|
||||||
|
|
||||||
|
mod cardgeninfo;
|
||||||
mod fields;
|
mod fields;
|
||||||
mod schema11;
|
mod schema11;
|
||||||
mod schemachange;
|
mod schemachange;
|
||||||
|
@ -11,6 +12,7 @@ pub use crate::backend_proto::{
|
||||||
card_requirement::CardRequirementKind, CardRequirement, CardTemplateConfig, NoteFieldConfig,
|
card_requirement::CardRequirementKind, CardRequirement, CardTemplateConfig, NoteFieldConfig,
|
||||||
NoteType as NoteTypeProto, NoteTypeConfig, NoteTypeKind,
|
NoteType as NoteTypeProto, NoteTypeConfig, NoteTypeKind,
|
||||||
};
|
};
|
||||||
|
pub(crate) use cardgeninfo::CardGenContext;
|
||||||
pub use fields::NoteField;
|
pub use fields::NoteField;
|
||||||
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11};
|
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11};
|
||||||
pub use stock::all_stock_notetypes;
|
pub use stock::all_stock_notetypes;
|
||||||
|
@ -18,6 +20,7 @@ pub use templates::CardTemplate;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
|
decks::DeckID,
|
||||||
define_newtype,
|
define_newtype,
|
||||||
err::{AnkiError, Result},
|
err::{AnkiError, Result},
|
||||||
notes::Note,
|
notes::Note,
|
||||||
|
@ -35,6 +38,7 @@ pub(crate) const DEFAULT_CSS: &str = include_str!("styling.css");
|
||||||
pub(crate) const DEFAULT_LATEX_HEADER: &str = include_str!("header.tex");
|
pub(crate) const DEFAULT_LATEX_HEADER: &str = include_str!("header.tex");
|
||||||
pub(crate) const DEFAULT_LATEX_FOOTER: &str = r"\end{document}";
|
pub(crate) const DEFAULT_LATEX_FOOTER: &str = r"\end{document}";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct NoteType {
|
pub struct NoteType {
|
||||||
pub id: NoteTypeID,
|
pub id: NoteTypeID,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -165,7 +169,11 @@ impl NoteType {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_note(&self) -> Note {
|
pub fn new_note(&self) -> Note {
|
||||||
Note::new(self.id, self.fields.len())
|
Note::new(&self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target_deck_id(&self) -> DeckID {
|
||||||
|
DeckID(self.config.target_deck_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,4 +202,8 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_notetype_by_name(&mut self, name: &str) -> Result<Option<NoteType>> {
|
||||||
|
self.storage.get_notetype_by_name(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl Collection {
|
||||||
})
|
})
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.collect();
|
.collect();
|
||||||
note.prepare_for_update(nt.config.sort_field_idx as usize, usn);
|
note.prepare_for_update(nt, usn)?;
|
||||||
self.storage.update_note(¬e)?;
|
self.storage.update_note(¬e)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -3,10 +3,13 @@
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend_proto::{CardTemplate as CardTemplateProto, CardTemplateConfig, OptionalUInt32},
|
backend_proto::{CardTemplate as CardTemplateProto, CardTemplateConfig, OptionalUInt32},
|
||||||
|
decks::DeckID,
|
||||||
|
template::ParsedTemplate,
|
||||||
timestamp::TimestampSecs,
|
timestamp::TimestampSecs,
|
||||||
types::Usn,
|
types::Usn,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct CardTemplate {
|
pub struct CardTemplate {
|
||||||
pub ord: Option<u32>,
|
pub ord: Option<u32>,
|
||||||
pub mtime_secs: TimestampSecs,
|
pub mtime_secs: TimestampSecs,
|
||||||
|
@ -15,6 +18,20 @@ pub struct CardTemplate {
|
||||||
pub config: CardTemplateConfig,
|
pub config: CardTemplateConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CardTemplate {
|
||||||
|
pub(crate) fn parsed_question(&self) -> Option<ParsedTemplate<'_>> {
|
||||||
|
ParsedTemplate::from_text(&self.config.q_format).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn target_deck_id(&self) -> Option<DeckID> {
|
||||||
|
if self.config.target_deck_id > 0 {
|
||||||
|
Some(DeckID(self.config.target_deck_id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<CardTemplate> for CardTemplateProto {
|
impl From<CardTemplate> for CardTemplateProto {
|
||||||
fn from(t: CardTemplate) -> Self {
|
fn from(t: CardTemplate) -> Self {
|
||||||
CardTemplateProto {
|
CardTemplateProto {
|
||||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
timestamp::TimestampMillis,
|
timestamp::TimestampMillis,
|
||||||
};
|
};
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use rusqlite::{params, Row, NO_PARAMS};
|
use rusqlite::{params, OptionalExtension, Row, NO_PARAMS};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
|
|
||||||
|
@ -36,11 +36,16 @@ impl SqliteStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_all_notetype_core(&self) -> Result<HashMap<NoteTypeID, NoteType>> {
|
pub(crate) fn get_all_notetype_core(&self) -> Result<HashMap<NoteTypeID, NoteType>> {
|
||||||
self.db
|
let mut nts: HashMap<NoteTypeID, NoteType> = self
|
||||||
|
.db
|
||||||
.prepare_cached(include_str!("get_notetype.sql"))?
|
.prepare_cached(include_str!("get_notetype.sql"))?
|
||||||
.query_and_then(NO_PARAMS, row_to_notetype_core)?
|
.query_and_then(NO_PARAMS, row_to_notetype_core)?
|
||||||
.map(|ntres| ntres.map(|nt| (nt.id, nt)))
|
.map(|ntres| ntres.map(|nt| (nt.id, nt)))
|
||||||
.collect()
|
.collect::<Result<_>>()?;
|
||||||
|
for nt in nts.values_mut() {
|
||||||
|
nt.fields = self.get_notetype_fields(nt.id)?;
|
||||||
|
}
|
||||||
|
Ok(nts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_notetype_fields(&self, ntid: NoteTypeID) -> Result<Vec<NoteField>> {
|
pub(crate) fn get_notetype_fields(&self, ntid: NoteTypeID) -> Result<Vec<NoteField>> {
|
||||||
|
@ -84,6 +89,22 @@ impl SqliteStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_notetype_id(&self, name: &str) -> Result<Option<NoteTypeID>> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("select id from notetypes where name = ?")?
|
||||||
|
.query_row(params![name], |row| row.get(0))
|
||||||
|
.optional()
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_notetype_by_name(&mut self, name: &str) -> Result<Option<NoteType>> {
|
||||||
|
if let Some(id) = self.get_notetype_id(name)? {
|
||||||
|
self.get_full_notetype(id)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn get_all_notetype_names(&self) -> Result<Vec<(NoteTypeID, String)>> {
|
fn get_all_notetype_names(&self) -> Result<Vec<(NoteTypeID, String)>> {
|
||||||
self.db
|
self.db
|
||||||
|
|
Loading…
Reference in a new issue