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:
Damien Elmes 2020-04-17 14:36:45 +10:00
parent 5d4f9dc3c0
commit f75fd5335d
11 changed files with 247 additions and 27 deletions

View file

@ -78,6 +78,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
AnkiError::SchemaChange => V::InvalidInput(pb::Empty {}),
AnkiError::JSONError { info } => V::JsonError(info),
AnkiError::ProtoError { info } => V::ProtoError(info),
AnkiError::NoCardsGenerated => todo!(),
};
pb::BackendError {

View file

@ -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 {
#[cfg(test)]
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)
}
#[allow(dead_code)]
pub(crate) fn add_card(&mut self, card: &mut Card) -> Result<()> {
if card.id.0 != 0 {
return Err(AnkiError::invalid_input("card id already set"));

View file

@ -8,7 +8,7 @@ use std::io;
pub type Result<T> = std::result::Result<T, AnkiError>;
#[derive(Debug, Fail)]
#[derive(Debug, Fail, PartialEq)]
pub enum AnkiError {
#[fail(display = "invalid input: {}", info)]
InvalidInput { info: String },
@ -48,6 +48,9 @@ pub enum AnkiError {
#[fail(display = "Operation modifies schema, but schema not marked modified.")]
SchemaChange,
#[fail(display = "No cards generated.")]
NoCardsGenerated,
}
// error helpers

View file

@ -404,7 +404,7 @@ where
&self.mgr.media_folder,
)? {
// 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(&note)?;
collection_modified = true;
}

View file

@ -1,11 +1,16 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::err::{AnkiError, Result};
use crate::notetype::NoteTypeID;
use crate::text::strip_html_preserving_image_filenames;
use crate::timestamp::TimestampSecs;
use crate::{collection::Collection, define_newtype, types::Usn};
use crate::{
card::Card,
collection::Collection,
define_newtype,
err::{AnkiError, Result},
notetype::{CardGenContext, NoteField, NoteType, NoteTypeID},
text::strip_html_preserving_image_filenames,
timestamp::TimestampSecs,
types::Usn,
};
use num_integer::Integer;
use std::{collections::HashSet, convert::TryInto};
@ -27,15 +32,15 @@ pub struct Note {
}
impl Note {
pub(crate) fn new(ntid: NoteTypeID, field_count: usize) -> Self {
pub(crate) fn new(notetype: &NoteType) -> Self {
Note {
id: NoteID(0),
guid: guid(),
ntid,
ntid: notetype.id,
mtime: TimestampSecs(0),
usn: Usn(0),
tags: vec![],
fields: vec!["".to_string(); field_count],
fields: vec!["".to_string(); notetype.fields.len()],
sort_field: None,
checksum: None,
}
@ -57,15 +62,24 @@ impl Note {
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 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
} else {
strip_html_preserving_image_filenames(
self.fields
.get(sort_field_idx)
.get(nt.config.sort_field_idx as usize)
.map(AsRef::as_ref)
.unwrap_or(""),
)
@ -74,10 +88,10 @@ impl Note {
self.checksum = Some(checksum);
self.mtime = TimestampSecs::now();
self.usn = usn;
Ok(())
}
#[allow(dead_code)]
pub(crate) fn nonempty_fields(&self) -> HashSet<u16> {
pub(crate) fn nonempty_fields<'a>(&self, fields: &'a [NoteField]) -> HashSet<&'a str> {
self.fields
.iter()
.enumerate()
@ -85,7 +99,7 @@ impl Note {
if s.trim().is_empty() {
None
} else {
Some(ord as u16)
fields.get(ord).map(|f| f.name.as_str())
}
})
.collect()
@ -105,7 +119,7 @@ fn guid() -> String {
fn anki_base91(mut n: u64) -> String {
let table = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\
0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~";
0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~";
let mut buf = String::new();
while n > 0 {
let (q, r) = n.div_rem(&(table.len() as u64));
@ -119,17 +133,50 @@ fn anki_base91(mut n: u64) -> String {
impl Collection {
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
self.transact(None, |col| {
println!("fixme: need to add cards, abort if no cards generated, etc");
// fixme: proper index
note.prepare_for_update(0, col.usn()?);
col.storage.add_note(note)
let nt = col
.storage
.get_full_notetype(note.ntid)?
.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)]
mod test {
use super::{anki_base91, field_checksum};
use crate::{
collection::open_test_collection,
err::{AnkiError, Result},
search::SortMode,
};
#[test]
fn test_base91() {
@ -145,4 +192,29 @@ mod test {
assert_eq!(field_checksum("test"), 2840236005);
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(())
}
}

View 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()
}
}

View file

@ -3,6 +3,7 @@
use crate::backend_proto::{NoteField as NoteFieldProto, NoteFieldConfig, OptionalUInt32};
#[derive(Debug)]
pub struct NoteField {
pub ord: Option<u32>,
pub name: String,

View file

@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod cardgeninfo;
mod fields;
mod schema11;
mod schemachange;
@ -11,6 +12,7 @@ pub use crate::backend_proto::{
card_requirement::CardRequirementKind, CardRequirement, CardTemplateConfig, NoteFieldConfig,
NoteType as NoteTypeProto, NoteTypeConfig, NoteTypeKind,
};
pub(crate) use cardgeninfo::CardGenContext;
pub use fields::NoteField;
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11};
pub use stock::all_stock_notetypes;
@ -18,6 +20,7 @@ pub use templates::CardTemplate;
use crate::{
collection::Collection,
decks::DeckID,
define_newtype,
err::{AnkiError, Result},
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_FOOTER: &str = r"\end{document}";
#[derive(Debug)]
pub struct NoteType {
pub id: NoteTypeID,
pub name: String,
@ -165,7 +169,11 @@ impl NoteType {
}
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(())
})
}
pub fn get_notetype_by_name(&mut self, name: &str) -> Result<Option<NoteType>> {
self.storage.get_notetype_by_name(name)
}
}

View file

@ -53,7 +53,7 @@ impl Collection {
})
.map(Into::into)
.collect();
note.prepare_for_update(nt.config.sort_field_idx as usize, usn);
note.prepare_for_update(nt, usn)?;
self.storage.update_note(&note)?;
}
Ok(())

View file

@ -3,10 +3,13 @@
use crate::{
backend_proto::{CardTemplate as CardTemplateProto, CardTemplateConfig, OptionalUInt32},
decks::DeckID,
template::ParsedTemplate,
timestamp::TimestampSecs,
types::Usn,
};
#[derive(Debug)]
pub struct CardTemplate {
pub ord: Option<u32>,
pub mtime_secs: TimestampSecs,
@ -15,6 +18,20 @@ pub struct CardTemplate {
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 {
fn from(t: CardTemplate) -> Self {
CardTemplateProto {

View file

@ -9,7 +9,7 @@ use crate::{
timestamp::TimestampMillis,
};
use prost::Message;
use rusqlite::{params, Row, NO_PARAMS};
use rusqlite::{params, OptionalExtension, Row, NO_PARAMS};
use std::collections::{HashMap, HashSet};
use unicase::UniCase;
@ -36,11 +36,16 @@ impl SqliteStorage {
}
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"))?
.query_and_then(NO_PARAMS, row_to_notetype_core)?
.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>> {
@ -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)]
fn get_all_notetype_names(&self) -> Result<Vec<(NoteTypeID, String)>> {
self.db