mirror of
https://github.com/ankitects/anki.git
synced 2025-12-10 21:36:55 -05:00
start on exposing notes and individual note type methods
changes to note: - add_note() now takes a provided deck id instead of looking it up in the notetype - note type use counts fetched using a single table scan - make sure note type changes are persisted - expose optionalness of ords in templates and fields json
This commit is contained in:
parent
09db596009
commit
6cc2bdbf87
10 changed files with 346 additions and 65 deletions
|
|
@ -69,11 +69,20 @@ message BackendInput {
|
||||||
SetConfigJson set_config_json = 53;
|
SetConfigJson set_config_json = 53;
|
||||||
bytes set_all_config = 54;
|
bytes set_all_config = 54;
|
||||||
Empty get_all_config = 55;
|
Empty get_all_config = 55;
|
||||||
Empty get_all_notetypes = 56;
|
int32 get_changed_notetypes = 56;
|
||||||
bytes set_all_notetypes = 57;
|
AddOrUpdateNotetypeIn add_or_update_notetype = 57;
|
||||||
Empty get_all_decks = 58;
|
Empty get_all_decks = 58;
|
||||||
bytes set_all_decks = 59;
|
bytes set_all_decks = 59;
|
||||||
Empty all_stock_notetypes = 60;
|
Empty all_stock_notetypes = 60;
|
||||||
|
int64 get_notetype_legacy = 61;
|
||||||
|
Empty get_notetype_names = 62;
|
||||||
|
Empty get_notetype_names_and_counts = 63;
|
||||||
|
string get_notetype_id_by_name = 64;
|
||||||
|
int64 remove_notetype = 65;
|
||||||
|
int64 new_note = 66;
|
||||||
|
AddNoteIn add_note = 67;
|
||||||
|
Note update_note = 68;
|
||||||
|
int64 get_note = 69;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,10 +132,19 @@ message BackendOutput {
|
||||||
Empty set_config_json = 53;
|
Empty set_config_json = 53;
|
||||||
Empty set_all_config = 54;
|
Empty set_all_config = 54;
|
||||||
bytes get_all_config = 55;
|
bytes get_all_config = 55;
|
||||||
bytes get_all_notetypes = 56;
|
bytes get_changed_notetypes = 56;
|
||||||
Empty set_all_notetypes = 57;
|
int64 add_or_update_notetype = 57;
|
||||||
bytes get_all_decks = 58;
|
bytes get_all_decks = 58;
|
||||||
Empty set_all_decks = 59;
|
Empty set_all_decks = 59;
|
||||||
|
bytes get_notetype_legacy = 61;
|
||||||
|
NoteTypeNames get_notetype_names = 62;
|
||||||
|
NoteTypeUseCounts get_notetype_names_and_counts = 63;
|
||||||
|
int64 get_notetype_id_by_name = 64;
|
||||||
|
Empty remove_notetype = 65;
|
||||||
|
Note new_note = 66;
|
||||||
|
int64 add_note = 67;
|
||||||
|
Empty update_note = 68;
|
||||||
|
Note get_note = 69;
|
||||||
|
|
||||||
BackendError error = 2047;
|
BackendError error = 2047;
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +165,7 @@ message BackendError {
|
||||||
Empty interrupted = 8;
|
Empty interrupted = 8;
|
||||||
string json_error = 9;
|
string json_error = 9;
|
||||||
string proto_error = 10;
|
string proto_error = 10;
|
||||||
|
Empty not_found_error = 11;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -569,3 +588,43 @@ enum StockNoteType {
|
||||||
message AllStockNotetypesOut {
|
message AllStockNotetypesOut {
|
||||||
repeated NoteType notetypes = 1;
|
repeated NoteType notetypes = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message NoteTypeNames {
|
||||||
|
repeated NoteTypeNameID entries = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NoteTypeUseCounts {
|
||||||
|
repeated NoteTypeNameIDUseCount entries = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NoteTypeNameID {
|
||||||
|
int64 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message NoteTypeNameIDUseCount {
|
||||||
|
int64 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
uint32 use_count = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddOrUpdateNotetypeIn {
|
||||||
|
bytes json = 1;
|
||||||
|
bool preserve_usn_and_mtime = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Note {
|
||||||
|
int64 id = 1;
|
||||||
|
string guid = 2;
|
||||||
|
int64 ntid = 3;
|
||||||
|
uint32 mtime_secs = 4;
|
||||||
|
int32 usn = 5;
|
||||||
|
repeated string tags = 6;
|
||||||
|
repeated string fields = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddNoteIn {
|
||||||
|
Note note = 1;
|
||||||
|
int64 deck_id = 2;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ use crate::{
|
||||||
media::check::MediaChecker,
|
media::check::MediaChecker,
|
||||||
media::sync::MediaSyncProgress,
|
media::sync::MediaSyncProgress,
|
||||||
media::MediaManager,
|
media::MediaManager,
|
||||||
notes::NoteID,
|
notes::{Note, NoteID},
|
||||||
notetype::{all_stock_notetypes, NoteTypeID, NoteTypeSchema11},
|
notetype::{all_stock_notetypes, NoteType, NoteTypeID, NoteTypeSchema11},
|
||||||
sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today},
|
sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today},
|
||||||
sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span},
|
sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span},
|
||||||
search::SortMode,
|
search::SortMode,
|
||||||
|
|
@ -77,6 +77,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
|
||||||
AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}),
|
AnkiError::CollectionAlreadyOpen => 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::NotFound => V::NotFoundError(Empty {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
pb::BackendError {
|
pb::BackendError {
|
||||||
|
|
@ -304,10 +305,8 @@ impl Backend {
|
||||||
pb::Empty {}
|
pb::Empty {}
|
||||||
}),
|
}),
|
||||||
Value::GetAllConfig(_) => OValue::GetAllConfig(self.get_all_config()?),
|
Value::GetAllConfig(_) => OValue::GetAllConfig(self.get_all_config()?),
|
||||||
Value::GetAllNotetypes(_) => OValue::GetAllNotetypes(self.get_all_notetypes()?),
|
Value::GetChangedNotetypes(_) => {
|
||||||
Value::SetAllNotetypes(bytes) => {
|
OValue::GetChangedNotetypes(self.get_changed_notetypes()?)
|
||||||
self.set_all_notetypes(&bytes)?;
|
|
||||||
OValue::SetAllNotetypes(pb::Empty {})
|
|
||||||
}
|
}
|
||||||
Value::GetAllDecks(_) => OValue::GetAllDecks(self.get_all_decks()?),
|
Value::GetAllDecks(_) => OValue::GetAllDecks(self.get_all_decks()?),
|
||||||
Value::SetAllDecks(bytes) => {
|
Value::SetAllDecks(bytes) => {
|
||||||
|
|
@ -320,6 +319,31 @@ impl Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.collect(),
|
.collect(),
|
||||||
}),
|
}),
|
||||||
|
Value::GetNotetypeLegacy(id) => {
|
||||||
|
OValue::GetNotetypeLegacy(self.get_notetype_legacy(id)?)
|
||||||
|
}
|
||||||
|
Value::GetNotetypeNames(_) => OValue::GetNotetypeNames(self.get_notetype_names()?),
|
||||||
|
Value::GetNotetypeNamesAndCounts(_) => {
|
||||||
|
OValue::GetNotetypeNamesAndCounts(self.get_notetype_use_counts()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::GetNotetypeIdByName(name) => {
|
||||||
|
OValue::GetNotetypeIdByName(self.get_notetype_id_by_name(name)?)
|
||||||
|
}
|
||||||
|
Value::AddOrUpdateNotetype(input) => {
|
||||||
|
OValue::AddOrUpdateNotetype(self.add_or_update_notetype_legacy(input)?)
|
||||||
|
}
|
||||||
|
Value::RemoveNotetype(id) => {
|
||||||
|
self.remove_notetype(id)?;
|
||||||
|
OValue::RemoveNotetype(pb::Empty {})
|
||||||
|
}
|
||||||
|
Value::NewNote(ntid) => OValue::NewNote(self.new_note(ntid)?),
|
||||||
|
Value::AddNote(input) => OValue::AddNote(self.add_note(input)?),
|
||||||
|
Value::UpdateNote(note) => {
|
||||||
|
self.update_note(note)?;
|
||||||
|
OValue::UpdateNote(pb::Empty {})
|
||||||
|
}
|
||||||
|
Value::GetNote(nid) => OValue::GetNote(self.get_note(nid)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -856,21 +880,12 @@ impl Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_all_notetypes(&self, json: &[u8]) -> Result<()> {
|
fn get_changed_notetypes(&self) -> Result<Vec<u8>> {
|
||||||
let val: HashMap<NoteTypeID, NoteTypeSchema11> = serde_json::from_slice(json)?;
|
todo!("filter by usn");
|
||||||
self.with_col(|col| {
|
// self.with_col(|col| {
|
||||||
col.transact(None, |col| {
|
// let nts = col.storage.get_all_notetypes_as_schema11()?;
|
||||||
col.storage.set_schema11_notetypes(val)?;
|
// serde_json::to_vec(&nts).map_err(Into::into)
|
||||||
col.storage.upgrade_notetypes_to_schema15()
|
// })
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_all_notetypes(&self) -> Result<Vec<u8>> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
let nts = col.storage.get_all_notetypes_as_schema11()?;
|
|
||||||
serde_json::to_vec(&nts).map_err(Into::into)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_all_decks(&self, json: &[u8]) -> Result<()> {
|
fn set_all_decks(&self, json: &[u8]) -> Result<()> {
|
||||||
|
|
@ -884,6 +899,104 @@ impl Backend {
|
||||||
serde_json::to_vec(&decks).map_err(Into::into)
|
serde_json::to_vec(&decks).map_err(Into::into)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_notetype_names(&self) -> Result<pb::NoteTypeNames> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let entries: Vec<_> = col
|
||||||
|
.storage
|
||||||
|
.get_all_notetype_names()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, name)| pb::NoteTypeNameId { id: id.0, name })
|
||||||
|
.collect();
|
||||||
|
Ok(pb::NoteTypeNames { entries })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_notetype_use_counts(&self) -> Result<pb::NoteTypeUseCounts> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let entries: Vec<_> = col
|
||||||
|
.storage
|
||||||
|
.get_notetype_use_counts()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, name, use_count)| pb::NoteTypeNameIdUseCount {
|
||||||
|
id: id.0,
|
||||||
|
name,
|
||||||
|
use_count,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(pb::NoteTypeUseCounts { entries })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_notetype_legacy(&self, id: i64) -> Result<Vec<u8>> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let schema11: NoteTypeSchema11 = col
|
||||||
|
.storage
|
||||||
|
.get_notetype(NoteTypeID(id))?
|
||||||
|
.ok_or(AnkiError::NotFound)?
|
||||||
|
.into();
|
||||||
|
Ok(serde_json::to_vec(&schema11)?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_notetype_id_by_name(&self, name: String) -> Result<i64> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.storage
|
||||||
|
.get_notetype_id(&name)
|
||||||
|
.map(|nt| nt.unwrap_or(NoteTypeID(0)).0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_or_update_notetype_legacy(&self, input: pb::AddOrUpdateNotetypeIn) -> Result<i64> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?;
|
||||||
|
let mut nt: NoteType = legacy.into();
|
||||||
|
if nt.id.0 == 0 {
|
||||||
|
col.add_notetype(&mut nt)?;
|
||||||
|
} else {
|
||||||
|
col.update_notetype(&mut nt, input.preserve_usn_and_mtime)?;
|
||||||
|
}
|
||||||
|
Ok(nt.id.0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_notetype(&self, _id: i64) -> Result<()> {
|
||||||
|
println!("fixme: remove notetype");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_note(&self, ntid: i64) -> Result<pb::Note> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let nt = col
|
||||||
|
.get_notetype(NoteTypeID(ntid))?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
Ok(nt.new_note().into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_note(&self, input: pb::AddNoteIn) -> Result<i64> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into();
|
||||||
|
col.add_note(&mut note, DeckID(input.deck_id))
|
||||||
|
.map(|_| note.id.0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_note(&self, pbnote: pb::Note) -> Result<()> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
let mut note: Note = pbnote.into();
|
||||||
|
col.update_note(&mut note)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_note(&self, nid: i64) -> Result<pb::Note> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.storage
|
||||||
|
.get_note(NoteID(nid))?
|
||||||
|
.ok_or(AnkiError::NotFound)
|
||||||
|
.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ pub enum AnkiError {
|
||||||
|
|
||||||
#[fail(display = "Close the existing collection first.")]
|
#[fail(display = "Close the existing collection first.")]
|
||||||
CollectionAlreadyOpen,
|
CollectionAlreadyOpen,
|
||||||
|
|
||||||
|
#[fail(display = "A requested item was not found.")]
|
||||||
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
// error helpers
|
// error helpers
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
// 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::{
|
use crate::{
|
||||||
|
backend_proto as pb,
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
|
decks::DeckID,
|
||||||
define_newtype,
|
define_newtype,
|
||||||
err::{AnkiError, Result},
|
err::{AnkiError, Result},
|
||||||
notetype::{CardGenContext, NoteField, NoteType, NoteTypeID},
|
notetype::{CardGenContext, NoteField, NoteType, NoteTypeID},
|
||||||
|
|
@ -105,6 +107,36 @@ impl Note {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Note> for pb::Note {
|
||||||
|
fn from(n: Note) -> Self {
|
||||||
|
pb::Note {
|
||||||
|
id: n.id.0,
|
||||||
|
guid: n.guid,
|
||||||
|
ntid: n.ntid.0,
|
||||||
|
mtime_secs: n.mtime.0 as u32,
|
||||||
|
usn: n.usn.0,
|
||||||
|
tags: n.tags,
|
||||||
|
fields: n.fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::Note> for Note {
|
||||||
|
fn from(n: pb::Note) -> Self {
|
||||||
|
Note {
|
||||||
|
id: NoteID(n.id),
|
||||||
|
guid: n.guid,
|
||||||
|
ntid: NoteTypeID(n.ntid),
|
||||||
|
mtime: TimestampSecs(n.mtime_secs as i64),
|
||||||
|
usn: Usn(n.usn),
|
||||||
|
tags: n.tags,
|
||||||
|
fields: n.fields,
|
||||||
|
sort_field: None,
|
||||||
|
checksum: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Text must be passed to strip_html_preserving_image_filenames() by
|
/// Text must be passed to strip_html_preserving_image_filenames() by
|
||||||
/// caller prior to passing in here.
|
/// caller prior to passing in here.
|
||||||
pub(crate) fn field_checksum(text: &str) -> u32 {
|
pub(crate) fn field_checksum(text: &str) -> u32 {
|
||||||
|
|
@ -130,20 +162,37 @@ fn anki_base91(mut n: u64) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
|
fn canonify_note_tags(&self, note: &mut Note, usn: Usn) -> Result<()> {
|
||||||
|
// fixme: avoid the excess split/join
|
||||||
|
note.tags = self
|
||||||
|
.canonify_tags(¬e.tags.join(" "), usn)?
|
||||||
|
.0
|
||||||
|
.split(' ')
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_note(&mut self, note: &mut Note, did: DeckID) -> Result<()> {
|
||||||
self.transact(None, |col| {
|
self.transact(None, |col| {
|
||||||
let nt = col
|
let nt = col
|
||||||
.get_notetype(note.ntid)?
|
.get_notetype(note.ntid)?
|
||||||
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
||||||
let ctx = CardGenContext::new(&nt, col.usn()?);
|
let ctx = CardGenContext::new(&nt, col.usn()?);
|
||||||
col.add_note_inner(&ctx, note)
|
col.add_note_inner(&ctx, note, did)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_note_inner(&mut self, ctx: &CardGenContext, note: &mut Note) -> Result<()> {
|
pub(crate) fn add_note_inner(
|
||||||
|
&mut self,
|
||||||
|
ctx: &CardGenContext,
|
||||||
|
note: &mut Note,
|
||||||
|
did: DeckID,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.canonify_note_tags(note, ctx.usn)?;
|
||||||
note.prepare_for_update(&ctx.notetype, ctx.usn)?;
|
note.prepare_for_update(&ctx.notetype, ctx.usn)?;
|
||||||
self.storage.add_note(note)?;
|
self.storage.add_note(note)?;
|
||||||
self.generate_cards_for_new_note(ctx, note)
|
self.generate_cards_for_new_note(ctx, note, did)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
||||||
|
|
@ -161,6 +210,7 @@ impl Collection {
|
||||||
ctx: &CardGenContext,
|
ctx: &CardGenContext,
|
||||||
note: &mut Note,
|
note: &mut Note,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
self.canonify_note_tags(note, ctx.usn)?;
|
||||||
note.prepare_for_update(ctx.notetype, ctx.usn)?;
|
note.prepare_for_update(ctx.notetype, ctx.usn)?;
|
||||||
self.generate_cards_for_existing_note(ctx, note)?;
|
self.generate_cards_for_existing_note(ctx, note)?;
|
||||||
self.storage.update_note(note)?;
|
self.storage.update_note(note)?;
|
||||||
|
|
@ -198,7 +248,7 @@ mod test {
|
||||||
|
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
// if no cards are generated, 1 card is added
|
// if no cards are generated, 1 card is added
|
||||||
col.add_note(&mut note).unwrap();
|
col.add_note(&mut note, DeckID(1)).unwrap();
|
||||||
let existing = col.storage.existing_cards_for_note(note.id)?;
|
let existing = col.storage.existing_cards_for_note(note.id)?;
|
||||||
assert_eq!(existing.len(), 1);
|
assert_eq!(existing.len(), 1);
|
||||||
assert_eq!(existing[0].ord, 0);
|
assert_eq!(existing[0].ord, 0);
|
||||||
|
|
@ -220,7 +270,7 @@ mod test {
|
||||||
// cloze cards also generate card 0 if no clozes are found
|
// cloze cards also generate card 0 if no clozes are found
|
||||||
let nt = col.get_notetype_by_name("cloze")?.unwrap();
|
let nt = col.get_notetype_by_name("cloze")?.unwrap();
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
col.add_note(&mut note).unwrap();
|
col.add_note(&mut note, DeckID(1)).unwrap();
|
||||||
let existing = col.storage.existing_cards_for_note(note.id)?;
|
let existing = col.storage.existing_cards_for_note(note.id)?;
|
||||||
assert_eq!(existing.len(), 1);
|
assert_eq!(existing.len(), 1);
|
||||||
assert_eq!(existing[0].ord, 0);
|
assert_eq!(existing[0].ord, 0);
|
||||||
|
|
|
||||||
|
|
@ -204,8 +204,9 @@ impl Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &CardGenContext,
|
ctx: &CardGenContext,
|
||||||
note: &Note,
|
note: &Note,
|
||||||
|
target_deck_id: DeckID,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.generate_cards_for_note(ctx, note, false)
|
self.generate_cards_for_note(ctx, note, &[], Some(target_deck_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn generate_cards_for_existing_note(
|
pub(crate) fn generate_cards_for_existing_note(
|
||||||
|
|
@ -213,25 +214,22 @@ impl Collection {
|
||||||
ctx: &CardGenContext,
|
ctx: &CardGenContext,
|
||||||
note: &Note,
|
note: &Note,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.generate_cards_for_note(ctx, note, true)
|
let existing = self.storage.existing_cards_for_note(note.id)?;
|
||||||
|
self.generate_cards_for_note(ctx, note, &existing, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_cards_for_note(
|
fn generate_cards_for_note(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &CardGenContext,
|
ctx: &CardGenContext,
|
||||||
note: &Note,
|
note: &Note,
|
||||||
check_existing: bool,
|
existing: &[AlreadyGeneratedCardInfo],
|
||||||
|
target_deck_id: Option<DeckID>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let existing = if check_existing {
|
|
||||||
self.storage.existing_cards_for_note(note.id)?
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
let cards = ctx.new_cards_required(note, &existing);
|
let cards = ctx.new_cards_required(note, &existing);
|
||||||
if cards.is_empty() {
|
if cards.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
self.add_generated_cards(ctx, note.id, &cards)
|
self.add_generated_cards(ctx, note.id, &cards, target_deck_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn generate_cards_for_notetype(&mut self, ctx: &CardGenContext) -> Result<()> {
|
pub(crate) fn generate_cards_for_notetype(&mut self, ctx: &CardGenContext) -> Result<()> {
|
||||||
|
|
@ -246,7 +244,7 @@ impl Collection {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let note = self.storage.get_note(nid)?.unwrap();
|
let note = self.storage.get_note(nid)?.unwrap();
|
||||||
self.generate_cards_for_note(ctx, ¬e, true)?;
|
self.generate_cards_for_note(ctx, ¬e, &existing_cards, None)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -257,11 +255,16 @@ impl Collection {
|
||||||
ctx: &CardGenContext,
|
ctx: &CardGenContext,
|
||||||
nid: NoteID,
|
nid: NoteID,
|
||||||
cards: &[CardToGenerate],
|
cards: &[CardToGenerate],
|
||||||
|
target_deck_id: Option<DeckID>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut next_pos = None;
|
let mut next_pos = None;
|
||||||
for c in cards {
|
for c in cards {
|
||||||
// fixme: deal with case where invalid deck pointed to
|
// fixme: deal with case where invalid deck pointed to
|
||||||
let did = c.did.unwrap_or_else(|| ctx.notetype.target_deck_id());
|
// fixme: deprecated note type deck id
|
||||||
|
let did = c
|
||||||
|
.did
|
||||||
|
.or(target_deck_id)
|
||||||
|
.unwrap_or_else(|| ctx.notetype.target_deck_id());
|
||||||
let due = c.due.unwrap_or_else(|| {
|
let due = c.due.unwrap_or_else(|| {
|
||||||
if next_pos.is_none() {
|
if next_pos.is_none() {
|
||||||
next_pos = Some(self.get_and_update_next_card_position().unwrap_or(0));
|
next_pos = Some(self.get_and_update_next_card_position().unwrap_or(0));
|
||||||
|
|
|
||||||
|
|
@ -168,16 +168,23 @@ impl NoteType {
|
||||||
|
|
||||||
pub(crate) fn prepare_for_adding(&mut self) -> Result<()> {
|
pub(crate) fn prepare_for_adding(&mut self) -> Result<()> {
|
||||||
// defaults to 0
|
// defaults to 0
|
||||||
self.config.target_deck_id = 1;
|
if self.config.target_deck_id == 0 {
|
||||||
|
self.config.target_deck_id = 1;
|
||||||
|
}
|
||||||
if self.fields.is_empty() {
|
if self.fields.is_empty() {
|
||||||
return Err(AnkiError::invalid_input("1 field required"));
|
return Err(AnkiError::invalid_input("1 field required"));
|
||||||
}
|
}
|
||||||
if self.templates.is_empty() {
|
if self.templates.is_empty() {
|
||||||
return Err(AnkiError::invalid_input("1 template required"));
|
return Err(AnkiError::invalid_input("1 template required"));
|
||||||
}
|
}
|
||||||
|
self.prepare_for_update()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepare_for_update(&mut self) -> Result<()> {
|
||||||
self.normalize_names();
|
self.normalize_names();
|
||||||
self.ensure_names_unique();
|
self.ensure_names_unique();
|
||||||
self.update_requirements();
|
self.update_requirements();
|
||||||
|
// fixme: deal with duplicate note type names on update
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,15 +212,34 @@ impl From<NoteType> for NoteTypeProto {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
|
/// Add a new notetype, and allocate it an ID.
|
||||||
|
pub fn add_notetype(&mut self, nt: &mut NoteType) -> Result<()> {
|
||||||
|
nt.prepare_for_adding()?;
|
||||||
|
self.transact(None, |col| col.storage.add_new_notetype(nt))
|
||||||
|
}
|
||||||
|
|
||||||
/// Saves changes to a note type. This will force a full sync if templates
|
/// Saves changes to a note type. This will force a full sync if templates
|
||||||
/// or fields have been added/removed/reordered.
|
/// or fields have been added/removed/reordered.
|
||||||
pub fn update_notetype(&mut self, nt: &mut NoteType) -> Result<()> {
|
pub fn update_notetype(&mut self, nt: &mut NoteType, preserve_usn: bool) -> Result<()> {
|
||||||
|
nt.prepare_for_update()?;
|
||||||
|
if !preserve_usn {
|
||||||
|
nt.mtime_secs = TimestampSecs::now();
|
||||||
|
nt.usn = self.usn()?;
|
||||||
|
}
|
||||||
self.transact(None, |col| {
|
self.transact(None, |col| {
|
||||||
let existing_notetype = col
|
if !preserve_usn {
|
||||||
.get_notetype(nt.id)?
|
let existing_notetype = col
|
||||||
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
|
.get_notetype(nt.id)?
|
||||||
col.update_notes_for_changed_fields(nt, existing_notetype.fields.len())?;
|
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
|
||||||
col.update_cards_for_changed_templates(nt, existing_notetype.templates.len())?;
|
col.update_notes_for_changed_fields(nt, existing_notetype.fields.len())?;
|
||||||
|
col.update_cards_for_changed_templates(nt, existing_notetype.templates.len())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.storage.update_notetype_config(&nt)?;
|
||||||
|
col.storage.update_notetype_fields(nt.id, &nt.fields)?;
|
||||||
|
col.storage
|
||||||
|
.update_notetype_templates(nt.id, &nt.templates)?;
|
||||||
|
|
||||||
// fixme: update cache instead of clearing
|
// fixme: update cache instead of clearing
|
||||||
col.state.notetype_cache.remove(&nt.id);
|
col.state.notetype_cache.remove(&nt.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ impl From<CardRequirement> for CardRequirementSchema11 {
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct NoteFieldSchema11 {
|
pub struct NoteFieldSchema11 {
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
pub(crate) ord: u16,
|
pub(crate) ord: Option<u16>,
|
||||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||||
pub(crate) sticky: bool,
|
pub(crate) sticky: bool,
|
||||||
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
#[serde(deserialize_with = "deserialize_bool_from_anything")]
|
||||||
|
|
@ -210,7 +210,7 @@ impl Default for NoteFieldSchema11 {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
ord: 0,
|
ord: None,
|
||||||
sticky: false,
|
sticky: false,
|
||||||
rtl: false,
|
rtl: false,
|
||||||
font: "Arial".to_string(),
|
font: "Arial".to_string(),
|
||||||
|
|
@ -223,7 +223,7 @@ impl Default for NoteFieldSchema11 {
|
||||||
impl From<NoteFieldSchema11> for NoteField {
|
impl From<NoteFieldSchema11> for NoteField {
|
||||||
fn from(f: NoteFieldSchema11) -> Self {
|
fn from(f: NoteFieldSchema11) -> Self {
|
||||||
NoteField {
|
NoteField {
|
||||||
ord: Some(f.ord as u32),
|
ord: f.ord.map(|o| o as u32),
|
||||||
name: f.name,
|
name: f.name,
|
||||||
config: NoteFieldConfig {
|
config: NoteFieldConfig {
|
||||||
sticky: f.sticky,
|
sticky: f.sticky,
|
||||||
|
|
@ -243,7 +243,7 @@ impl From<NoteField> for NoteFieldSchema11 {
|
||||||
let conf = p.config;
|
let conf = p.config;
|
||||||
NoteFieldSchema11 {
|
NoteFieldSchema11 {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
ord: p.ord.unwrap() as u16,
|
ord: p.ord.map(|o| o as u16),
|
||||||
sticky: conf.sticky,
|
sticky: conf.sticky,
|
||||||
rtl: conf.rtl,
|
rtl: conf.rtl,
|
||||||
font: conf.font_name,
|
font: conf.font_name,
|
||||||
|
|
@ -256,7 +256,7 @@ impl From<NoteField> for NoteFieldSchema11 {
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||||
pub struct CardTemplateSchema11 {
|
pub struct CardTemplateSchema11 {
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
pub(crate) ord: u16,
|
pub(crate) ord: Option<u16>,
|
||||||
pub(crate) qfmt: String,
|
pub(crate) qfmt: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) afmt: String,
|
pub(crate) afmt: String,
|
||||||
|
|
@ -277,7 +277,7 @@ pub struct CardTemplateSchema11 {
|
||||||
impl From<CardTemplateSchema11> for CardTemplate {
|
impl From<CardTemplateSchema11> for CardTemplate {
|
||||||
fn from(t: CardTemplateSchema11) -> Self {
|
fn from(t: CardTemplateSchema11) -> Self {
|
||||||
CardTemplate {
|
CardTemplate {
|
||||||
ord: Some(t.ord as u32),
|
ord: t.ord.map(|t| t as u32),
|
||||||
name: t.name,
|
name: t.name,
|
||||||
mtime_secs: TimestampSecs(0),
|
mtime_secs: TimestampSecs(0),
|
||||||
usn: Usn(0),
|
usn: Usn(0),
|
||||||
|
|
@ -302,7 +302,7 @@ impl From<CardTemplate> for CardTemplateSchema11 {
|
||||||
let conf = p.config;
|
let conf = p.config;
|
||||||
CardTemplateSchema11 {
|
CardTemplateSchema11 {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
ord: p.ord.unwrap() as u16,
|
ord: p.ord.map(|o| o as u16),
|
||||||
qfmt: conf.q_format,
|
qfmt: conf.q_format,
|
||||||
afmt: conf.a_format,
|
afmt: conf.a_format,
|
||||||
bqfmt: conf.q_format_browser,
|
bqfmt: conf.q_format_browser,
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ impl Collection {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{ords_changed, TemplateOrdChanges};
|
use super::{ords_changed, TemplateOrdChanges};
|
||||||
use crate::{collection::open_test_collection, err::Result, search::SortMode};
|
use crate::{collection::open_test_collection, decks::DeckID, err::Result, search::SortMode};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ord_changes() {
|
fn ord_changes() {
|
||||||
|
|
@ -190,10 +190,10 @@ mod test {
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
assert_eq!(note.fields.len(), 2);
|
assert_eq!(note.fields.len(), 2);
|
||||||
note.fields = vec!["one".into(), "two".into()];
|
note.fields = vec!["one".into(), "two".into()];
|
||||||
col.add_note(&mut note)?;
|
col.add_note(&mut note, DeckID(1))?;
|
||||||
|
|
||||||
nt.add_field("three");
|
nt.add_field("three");
|
||||||
col.update_notetype(&mut nt)?;
|
col.update_notetype(&mut nt, false)?;
|
||||||
|
|
||||||
let note = col.storage.get_note(note.id)?.unwrap();
|
let note = col.storage.get_note(note.id)?.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -202,7 +202,7 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
nt.fields.remove(1);
|
nt.fields.remove(1);
|
||||||
col.update_notetype(&mut nt)?;
|
col.update_notetype(&mut nt, false)?;
|
||||||
|
|
||||||
let note = col.storage.get_note(note.id)?.unwrap();
|
let note = col.storage.get_note(note.id)?.unwrap();
|
||||||
assert_eq!(note.fields, vec!["one".to_string(), "".into()]);
|
assert_eq!(note.fields, vec!["one".to_string(), "".into()]);
|
||||||
|
|
@ -220,7 +220,7 @@ mod test {
|
||||||
let mut note = nt.new_note();
|
let mut note = nt.new_note();
|
||||||
assert_eq!(note.fields.len(), 2);
|
assert_eq!(note.fields.len(), 2);
|
||||||
note.fields = vec!["one".into(), "two".into()];
|
note.fields = vec!["one".into(), "two".into()];
|
||||||
col.add_note(&mut note)?;
|
col.add_note(&mut note, DeckID(1))?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
||||||
|
|
@ -231,7 +231,7 @@ mod test {
|
||||||
|
|
||||||
// add an extra card template
|
// add an extra card template
|
||||||
nt.add_template("card 2", "{{Front}}", "");
|
nt.add_template("card 2", "{{Front}}", "");
|
||||||
col.update_notetype(&mut nt)?;
|
col.update_notetype(&mut nt, false)?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
||||||
|
|
|
||||||
15
rslib/src/storage/notetype/get_use_counts.sql
Normal file
15
rslib/src/storage/notetype/get_use_counts.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
select
|
||||||
|
mid,
|
||||||
|
(
|
||||||
|
select
|
||||||
|
name
|
||||||
|
from notetypes nt
|
||||||
|
where
|
||||||
|
nt.id = mid
|
||||||
|
) as name,
|
||||||
|
count(id)
|
||||||
|
from notes
|
||||||
|
group by
|
||||||
|
mid
|
||||||
|
order by
|
||||||
|
name
|
||||||
|
|
@ -105,7 +105,19 @@ impl SqliteStorage {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_notetype_fields(&self, ntid: NoteTypeID, fields: &[NoteField]) -> Result<()> {
|
/// Returns list of (id, name, use_count)
|
||||||
|
pub fn get_notetype_use_counts(&self) -> Result<Vec<(NoteTypeID, String, u32)>> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached(include_str!("get_use_counts.sql"))?
|
||||||
|
.query_and_then(NO_PARAMS, |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_notetype_fields(
|
||||||
|
&self,
|
||||||
|
ntid: NoteTypeID,
|
||||||
|
fields: &[NoteField],
|
||||||
|
) -> Result<()> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("delete from fields where ntid=?")?
|
.prepare_cached("delete from fields where ntid=?")?
|
||||||
.execute(&[ntid])?;
|
.execute(&[ntid])?;
|
||||||
|
|
@ -119,7 +131,7 @@ impl SqliteStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_notetype_templates(
|
pub(crate) fn update_notetype_templates(
|
||||||
&self,
|
&self,
|
||||||
ntid: NoteTypeID,
|
ntid: NoteTypeID,
|
||||||
templates: &[CardTemplate],
|
templates: &[CardTemplate],
|
||||||
|
|
@ -146,7 +158,7 @@ impl SqliteStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_notetype_config(&self, nt: &NoteType) -> Result<()> {
|
pub(crate) fn update_notetype_config(&self, nt: &NoteType) -> Result<()> {
|
||||||
assert!(nt.id.0 != 0);
|
assert!(nt.id.0 != 0);
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.db
|
.db
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue