add notetype changing to backend
This commit is contained in:
parent
c5e56a5fe8
commit
1f2567e04c
@ -27,6 +27,7 @@ browsing-change-deck = Change Deck
|
|||||||
browsing-change-deck2 = Change Deck...
|
browsing-change-deck2 = Change Deck...
|
||||||
browsing-change-note-type = Change Note Type
|
browsing-change-note-type = Change Note Type
|
||||||
browsing-change-note-type2 = Change Note Type...
|
browsing-change-note-type2 = Change Note Type...
|
||||||
|
browsing-change-notetype = Change Notetype
|
||||||
browsing-change-to = Change { $val } to:
|
browsing-change-to = Change { $val } to:
|
||||||
browsing-clear-unused = Clear Unused
|
browsing-clear-unused = Clear Unused
|
||||||
browsing-clear-unused-tags = Clear Unused Tags
|
browsing-clear-unused-tags = Clear Unused Tags
|
||||||
|
@ -7,3 +7,4 @@ errors-100-tags-max =
|
|||||||
A maximum of 100 tags can be selected. Listing the
|
A maximum of 100 tags can be selected. Listing the
|
||||||
tags you want instead of the ones you don't want is usually simpler, and there
|
tags you want instead of the ones you don't want is usually simpler, and there
|
||||||
is no need to select child tags if you have selected a parent tag.
|
is no need to select child tags if you have selected a parent tag.
|
||||||
|
errors-multiple-notetypes-selected = Please select notes from only one notetype.
|
||||||
|
@ -17,6 +17,10 @@ message OptionalUInt32 {
|
|||||||
uint32 val = 1;
|
uint32 val = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message OptionalUInt32Wrapper {
|
||||||
|
OptionalUInt32 inner = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message Int32 {
|
message Int32 {
|
||||||
sint32 val = 1;
|
sint32 val = 1;
|
||||||
}
|
}
|
||||||
@ -216,6 +220,9 @@ service NotetypesService {
|
|||||||
rpc RemoveNotetype(NotetypeId) returns (OpChanges);
|
rpc RemoveNotetype(NotetypeId) returns (OpChanges);
|
||||||
rpc GetAuxNotetypeConfigKey(GetAuxConfigKeyIn) returns (String);
|
rpc GetAuxNotetypeConfigKey(GetAuxConfigKeyIn) returns (String);
|
||||||
rpc GetAuxTemplateConfigKey(GetAuxTemplateConfigKeyIn) returns (String);
|
rpc GetAuxTemplateConfigKey(GetAuxTemplateConfigKeyIn) returns (String);
|
||||||
|
rpc GetChangeNotetypeInfo(GetChangeNotetypeInfoIn)
|
||||||
|
returns (ChangeNotetypeInfo);
|
||||||
|
rpc ChangeNotetype(ChangeNotetypeIn) returns (OpChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
service CardRenderingService {
|
service CardRenderingService {
|
||||||
@ -1636,3 +1643,25 @@ message GetAuxTemplateConfigKeyIn {
|
|||||||
uint32 card_ordinal = 2;
|
uint32 card_ordinal = 2;
|
||||||
string key = 3;
|
string key = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetChangeNotetypeInfoIn {
|
||||||
|
repeated int64 note_ids = 1;
|
||||||
|
int64 new_notetype_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChangeNotetypeIn {
|
||||||
|
repeated int64 note_ids = 1;
|
||||||
|
repeated OptionalUInt32Wrapper new_fields = 2;
|
||||||
|
repeated OptionalUInt32Wrapper new_templates = 3;
|
||||||
|
int64 old_notetype_id = 4;
|
||||||
|
int64 new_notetype_id = 5;
|
||||||
|
int64 current_schema = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChangeNotetypeInfo {
|
||||||
|
repeated string old_field_names = 1;
|
||||||
|
repeated string old_template_names = 2;
|
||||||
|
repeated string new_field_names = 3;
|
||||||
|
repeated string new_template_names = 4;
|
||||||
|
ChangeNotetypeIn input = 5;
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ impl AnkiError {
|
|||||||
AnkiError::ParseNumError => Kind::InvalidInput,
|
AnkiError::ParseNumError => Kind::InvalidInput,
|
||||||
AnkiError::InvalidRegex(_) => Kind::InvalidInput,
|
AnkiError::InvalidRegex(_) => Kind::InvalidInput,
|
||||||
AnkiError::UndoEmpty => Kind::UndoEmpty,
|
AnkiError::UndoEmpty => Kind::UndoEmpty,
|
||||||
|
AnkiError::MultipleNotetypesSelected => Kind::InvalidInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
pb::BackendError {
|
pb::BackendError {
|
||||||
|
@ -131,7 +131,7 @@ impl NotesService for Backend {
|
|||||||
fn cards_of_note(&self, input: pb::NoteId) -> Result<pb::CardIds> {
|
fn cards_of_note(&self, input: pb::NoteId) -> Result<pb::CardIds> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.storage
|
col.storage
|
||||||
.all_card_ids_of_note_in_order(NoteId(input.nid))
|
.all_card_ids_of_note_in_template_order(NoteId(input.nid))
|
||||||
.map(|v| pb::CardIds {
|
.map(|v| pb::CardIds {
|
||||||
cids: v.into_iter().map(Into::into).collect(),
|
cids: v.into_iter().map(Into::into).collect(),
|
||||||
})
|
})
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
// 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 super::Backend;
|
use super::{notes::to_note_ids, Backend};
|
||||||
pub(super) use crate::backend_proto::notetypes_service::Service as NotetypesService;
|
pub(super) use crate::backend_proto::notetypes_service::Service as NotetypesService;
|
||||||
use crate::{
|
use crate::{
|
||||||
backend_proto as pb,
|
backend_proto as pb,
|
||||||
config::get_aux_notetype_config_key,
|
config::get_aux_notetype_config_key,
|
||||||
notetype::{all_stock_notetypes, Notetype, NotetypeSchema11},
|
notetype::{
|
||||||
|
all_stock_notetypes, ChangeNotetypeInput, Notetype, NotetypeChangeInfo, NotetypeSchema11,
|
||||||
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -153,6 +155,19 @@ impl NotetypesService for Backend {
|
|||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_change_notetype_info(
|
||||||
|
&self,
|
||||||
|
input: pb::GetChangeNotetypeInfoIn,
|
||||||
|
) -> Result<pb::ChangeNotetypeInfo> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.notetype_change_info(to_note_ids(input.note_ids), input.new_notetype_id.into())
|
||||||
|
.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn change_notetype(&self, input: pb::ChangeNotetypeIn) -> Result<pb::OpChanges> {
|
||||||
|
self.with_col(|col| col.change_notetype_of_notes(input.into()).map(Into::into))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<pb::Notetype> for Notetype {
|
impl From<pb::Notetype> for Notetype {
|
||||||
@ -168,3 +183,69 @@ impl From<pb::Notetype> for Notetype {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<NotetypeChangeInfo> for pb::ChangeNotetypeInfo {
|
||||||
|
fn from(i: NotetypeChangeInfo) -> Self {
|
||||||
|
pb::ChangeNotetypeInfo {
|
||||||
|
old_field_names: i.old_field_names,
|
||||||
|
old_template_names: i.old_template_names,
|
||||||
|
new_field_names: i.new_field_names,
|
||||||
|
new_template_names: i.new_template_names,
|
||||||
|
input: Some(i.input.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pb::ChangeNotetypeIn> for ChangeNotetypeInput {
|
||||||
|
fn from(i: pb::ChangeNotetypeIn) -> Self {
|
||||||
|
ChangeNotetypeInput {
|
||||||
|
current_schema: i.current_schema.into(),
|
||||||
|
note_ids: i.note_ids.into_newtype(NoteId),
|
||||||
|
old_notetype_id: i.old_notetype_id.into(),
|
||||||
|
new_notetype_id: i.new_notetype_id.into(),
|
||||||
|
new_fields: i
|
||||||
|
.new_fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|wrapper| wrapper.inner.map(|v| v.val as usize))
|
||||||
|
.collect(),
|
||||||
|
new_templates: {
|
||||||
|
let v: Vec<_> = i
|
||||||
|
.new_templates
|
||||||
|
.into_iter()
|
||||||
|
.map(|wrapper| wrapper.inner.map(|v| v.val as usize))
|
||||||
|
.collect();
|
||||||
|
if v.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChangeNotetypeInput> for pb::ChangeNotetypeIn {
|
||||||
|
fn from(i: ChangeNotetypeInput) -> Self {
|
||||||
|
pb::ChangeNotetypeIn {
|
||||||
|
current_schema: i.current_schema.into(),
|
||||||
|
note_ids: i.note_ids.into_iter().map(Into::into).collect(),
|
||||||
|
old_notetype_id: i.old_notetype_id.into(),
|
||||||
|
new_notetype_id: i.new_notetype_id.into(),
|
||||||
|
new_fields: i
|
||||||
|
.new_fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|idx| pb::OptionalUInt32Wrapper {
|
||||||
|
inner: idx.map(|idx| pb::OptionalUInt32 { val: idx as u32 }),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
new_templates: i
|
||||||
|
.new_templates
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|idx| pb::OptionalUInt32Wrapper {
|
||||||
|
inner: idx.map(|idx| pb::OptionalUInt32 { val: idx as u32 }),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -39,6 +39,7 @@ pub enum AnkiError {
|
|||||||
SearchError(SearchErrorKind),
|
SearchError(SearchErrorKind),
|
||||||
InvalidRegex(String),
|
InvalidRegex(String),
|
||||||
UndoEmpty,
|
UndoEmpty,
|
||||||
|
MultipleNotetypesSelected,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AnkiError {
|
impl Display for AnkiError {
|
||||||
@ -90,7 +91,16 @@ impl AnkiError {
|
|||||||
AnkiError::ParseNumError => tr.errors_parse_number_fail().into(),
|
AnkiError::ParseNumError => tr.errors_parse_number_fail().into(),
|
||||||
AnkiError::FilteredDeckError(err) => err.localized_description(tr),
|
AnkiError::FilteredDeckError(err) => err.localized_description(tr),
|
||||||
AnkiError::InvalidRegex(err) => format!("<pre>{}</pre>", err),
|
AnkiError::InvalidRegex(err) => format!("<pre>{}</pre>", err),
|
||||||
_ => format!("{:?}", self),
|
AnkiError::MultipleNotetypesSelected => tr.errors_multiple_notetypes_selected().into(),
|
||||||
|
AnkiError::IoError(_)
|
||||||
|
| AnkiError::JsonError(_)
|
||||||
|
| AnkiError::ProtoError(_)
|
||||||
|
| AnkiError::Interrupted
|
||||||
|
| AnkiError::CollectionNotOpen
|
||||||
|
| AnkiError::CollectionAlreadyOpen
|
||||||
|
| AnkiError::NotFound
|
||||||
|
| AnkiError::Existing
|
||||||
|
| AnkiError::UndoEmpty => format!("{:?}", self),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -419,7 +419,7 @@ impl Collection {
|
|||||||
&mut self,
|
&mut self,
|
||||||
note: &mut Note,
|
note: &mut Note,
|
||||||
original: &Note,
|
original: &Note,
|
||||||
nt: &Notetype,
|
notetype: &Notetype,
|
||||||
usn: Usn,
|
usn: Usn,
|
||||||
mark_note_modified: bool,
|
mark_note_modified: bool,
|
||||||
normalize_text: bool,
|
normalize_text: bool,
|
||||||
@ -428,7 +428,7 @@ impl Collection {
|
|||||||
if update_tags {
|
if update_tags {
|
||||||
self.canonify_note_tags(note, usn)?;
|
self.canonify_note_tags(note, usn)?;
|
||||||
}
|
}
|
||||||
note.prepare_for_update(nt, normalize_text)?;
|
note.prepare_for_update(notetype, normalize_text)?;
|
||||||
if mark_note_modified {
|
if mark_note_modified {
|
||||||
note.set_modified(usn);
|
note.set_modified(usn);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
mod cardgen;
|
mod cardgen;
|
||||||
mod emptycards;
|
mod emptycards;
|
||||||
mod fields;
|
mod fields;
|
||||||
|
mod notetypechange;
|
||||||
mod render;
|
mod render;
|
||||||
mod schema11;
|
mod schema11;
|
||||||
mod schemachange;
|
mod schemachange;
|
||||||
@ -11,7 +12,6 @@ mod stock;
|
|||||||
mod templates;
|
mod templates;
|
||||||
pub(crate) mod undo;
|
pub(crate) mod undo;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
iter::FromIterator,
|
iter::FromIterator,
|
||||||
@ -20,6 +20,8 @@ use std::{
|
|||||||
|
|
||||||
pub(crate) use cardgen::{AlreadyGeneratedCardInfo, CardGenContext};
|
pub(crate) use cardgen::{AlreadyGeneratedCardInfo, CardGenContext};
|
||||||
pub use fields::NoteField;
|
pub use fields::NoteField;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
pub use notetypechange::{ChangeNotetypeInput, NotetypeChangeInfo};
|
||||||
pub(crate) use render::RenderCardOutput;
|
pub(crate) use render::RenderCardOutput;
|
||||||
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NotetypeSchema11};
|
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NotetypeSchema11};
|
||||||
pub use stock::all_stock_notetypes;
|
pub use stock::all_stock_notetypes;
|
||||||
@ -123,6 +125,9 @@ impl Collection {
|
|||||||
|
|
||||||
/// 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.
|
||||||
|
///
|
||||||
|
/// This does not assign ordinals to the provided notetype, so if you wish
|
||||||
|
/// to make use of template_idx, the notetype must be fetched again.
|
||||||
pub fn update_notetype(&mut self, notetype: &mut Notetype) -> Result<OpOutput<()>> {
|
pub fn update_notetype(&mut self, notetype: &mut Notetype) -> Result<OpOutput<()>> {
|
||||||
self.transact(Op::UpdateNotetype, |col| {
|
self.transact(Op::UpdateNotetype, |col| {
|
||||||
let original = col
|
let original = col
|
||||||
|
549
rslib/src/notetype/notetypechange.rs
Normal file
549
rslib/src/notetype/notetypechange.rs
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// Lice removed: (), remapped: () removed: (), remapped: () nse: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
//! Updates to notes/cards when a note is moved to a different notetype.
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use super::{CardGenContext, Notetype, NotetypeKind};
|
||||||
|
use crate::{
|
||||||
|
prelude::*,
|
||||||
|
search::{Node, SearchNode, SortMode, TemplateKind},
|
||||||
|
storage::comma_separated_ids,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChangeNotetypeInput {
|
||||||
|
pub current_schema: TimestampMillis,
|
||||||
|
pub note_ids: Vec<NoteId>,
|
||||||
|
pub old_notetype_id: NotetypeId,
|
||||||
|
pub new_notetype_id: NotetypeId,
|
||||||
|
pub new_fields: Vec<Option<usize>>,
|
||||||
|
pub new_templates: Option<Vec<Option<usize>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NotetypeChangeInfo {
|
||||||
|
pub input: ChangeNotetypeInput,
|
||||||
|
pub old_field_names: Vec<String>,
|
||||||
|
pub old_template_names: Vec<String>,
|
||||||
|
pub new_field_names: Vec<String>,
|
||||||
|
pub new_template_names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct TemplateMap {
|
||||||
|
pub removed: Vec<usize>,
|
||||||
|
pub remapped: HashMap<usize, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateMap {
|
||||||
|
fn new(new_templates: Vec<Option<usize>>, old_template_count: usize) -> Result<Self> {
|
||||||
|
let mut seen: HashSet<usize> = HashSet::new();
|
||||||
|
let remapped: HashMap<_, _> = new_templates
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(new_idx, old_idx)| {
|
||||||
|
if let Some(old_idx) = *old_idx {
|
||||||
|
seen.insert(old_idx);
|
||||||
|
if old_idx != new_idx {
|
||||||
|
return Some((old_idx, new_idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let removed: Vec<_> = (0..old_template_count)
|
||||||
|
.filter(|idx| !seen.contains(&idx))
|
||||||
|
.collect();
|
||||||
|
if removed.len() == new_templates.len() {
|
||||||
|
return Err(AnkiError::invalid_input(
|
||||||
|
"at least one template must be mapped",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TemplateMap { removed, remapped })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub fn notetype_change_info(
|
||||||
|
&mut self,
|
||||||
|
note_ids: Vec<NoteId>,
|
||||||
|
new_notetype_id: NotetypeId,
|
||||||
|
) -> Result<NotetypeChangeInfo> {
|
||||||
|
let old_notetype_id = self.get_single_notetype_of_notes(¬e_ids)?;
|
||||||
|
let old_notetype = self
|
||||||
|
.get_notetype(old_notetype_id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
let new_notetype = self
|
||||||
|
.get_notetype(new_notetype_id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
|
||||||
|
let current_schema = self.storage.get_collection_timestamps()?.schema_change;
|
||||||
|
let new_fields = default_field_map(&old_notetype, &new_notetype);
|
||||||
|
let new_templates = default_template_map(&old_notetype, &new_notetype);
|
||||||
|
|
||||||
|
Ok(NotetypeChangeInfo {
|
||||||
|
input: ChangeNotetypeInput {
|
||||||
|
current_schema,
|
||||||
|
note_ids,
|
||||||
|
old_notetype_id,
|
||||||
|
new_notetype_id,
|
||||||
|
new_fields,
|
||||||
|
new_templates,
|
||||||
|
},
|
||||||
|
old_field_names: old_notetype.fields.iter().map(|f| f.name.clone()).collect(),
|
||||||
|
old_template_names: old_notetype
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.name.clone())
|
||||||
|
.collect(),
|
||||||
|
new_field_names: new_notetype.fields.iter().map(|f| f.name.clone()).collect(),
|
||||||
|
new_template_names: new_notetype
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.map(|f| f.name.clone())
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_notetype_of_notes(&mut self, input: ChangeNotetypeInput) -> Result<OpOutput<()>> {
|
||||||
|
self.transact(Op::ChangeNotetype, |col| {
|
||||||
|
col.change_notetype_of_notes_inner(input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_template_map(
|
||||||
|
current_notetype: &Notetype,
|
||||||
|
new_notetype: &Notetype,
|
||||||
|
) -> Option<Vec<Option<usize>>> {
|
||||||
|
if current_notetype.config.kind() == NotetypeKind::Cloze
|
||||||
|
|| new_notetype.config.kind() == NotetypeKind::Cloze
|
||||||
|
{
|
||||||
|
// clozes can't be remapped
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// name -> (ordinal, is_used)
|
||||||
|
let mut existing_templates: HashMap<&str, (usize, bool)> = current_notetype
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.map(|template| {
|
||||||
|
(
|
||||||
|
template.name.as_str(),
|
||||||
|
(template.ord.unwrap() as usize, false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// match by name
|
||||||
|
let mut new_templates: Vec<_> = new_notetype
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.map(|template| {
|
||||||
|
existing_templates
|
||||||
|
.get_mut(template.name.as_str())
|
||||||
|
.map(|(idx, used)| {
|
||||||
|
*used = true;
|
||||||
|
*idx
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// fill in gaps with any unused templates
|
||||||
|
let mut remaining_templates: Vec<_> = existing_templates
|
||||||
|
.values()
|
||||||
|
.filter_map(|(idx, used)| if !used { Some(idx) } else { None })
|
||||||
|
.collect();
|
||||||
|
remaining_templates.sort_unstable();
|
||||||
|
new_templates
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|o| o.is_none())
|
||||||
|
.zip(remaining_templates.into_iter())
|
||||||
|
.for_each(|(template, old_idx)| *template = Some(*old_idx));
|
||||||
|
|
||||||
|
Some(new_templates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_field_map(current_notetype: &Notetype, new_notetype: &Notetype) -> Vec<Option<usize>> {
|
||||||
|
// name -> (ordinal, is_used)
|
||||||
|
let mut existing_fields: HashMap<&str, (usize, bool)> = current_notetype
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| (field.name.as_str(), (field.ord.unwrap() as usize, false)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// match by name
|
||||||
|
let mut new_fields: Vec<_> = new_notetype
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
|
existing_fields
|
||||||
|
.get_mut(field.name.as_str())
|
||||||
|
.map(|(idx, used)| {
|
||||||
|
*used = true;
|
||||||
|
*idx
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// fill in gaps with any unused fields
|
||||||
|
let mut remaining_fields: Vec<_> = existing_fields
|
||||||
|
.values()
|
||||||
|
.filter_map(|(idx, used)| if !used { Some(idx) } else { None })
|
||||||
|
.collect();
|
||||||
|
remaining_fields.sort_unstable();
|
||||||
|
new_fields
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|o| o.is_none())
|
||||||
|
.zip(remaining_fields.into_iter())
|
||||||
|
.for_each(|(field, old_idx)| *field = Some(*old_idx));
|
||||||
|
|
||||||
|
new_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
/// Return the notetype used by `note_ids`, or an error if not exactly 1
|
||||||
|
/// notetype is in use.
|
||||||
|
fn get_single_notetype_of_notes(&mut self, note_ids: &[NoteId]) -> Result<NotetypeId> {
|
||||||
|
if note_ids.is_empty() {
|
||||||
|
return Err(AnkiError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nids_node: Node = SearchNode::NoteIds(comma_separated_ids(¬e_ids)).into();
|
||||||
|
let note1 = self
|
||||||
|
.storage
|
||||||
|
.get_note(*note_ids.first().unwrap())?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
|
||||||
|
if self
|
||||||
|
.search_notes_unordered(match_all![note1.notetype_id, nids_node])?
|
||||||
|
.len()
|
||||||
|
!= note_ids.len()
|
||||||
|
{
|
||||||
|
Err(AnkiError::MultipleNotetypesSelected)
|
||||||
|
} else {
|
||||||
|
Ok(note1.notetype_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_notetype_of_notes_inner(&mut self, input: ChangeNotetypeInput) -> Result<()> {
|
||||||
|
if input.current_schema != self.storage.get_collection_timestamps()?.schema_change {
|
||||||
|
return Err(AnkiError::invalid_input("schema changed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let usn = self.usn()?;
|
||||||
|
self.set_schema_modified()?;
|
||||||
|
if let Some(new_templates) = input.new_templates {
|
||||||
|
let old_notetype = self
|
||||||
|
.get_notetype(input.old_notetype_id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
self.update_cards_for_new_notetype(
|
||||||
|
&input.note_ids,
|
||||||
|
old_notetype.templates.len(),
|
||||||
|
new_templates,
|
||||||
|
usn,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
self.maybe_remove_cards_with_missing_template(
|
||||||
|
&input.note_ids,
|
||||||
|
input.new_notetype_id,
|
||||||
|
usn,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
self.update_notes_for_new_notetype_and_generate_cards(
|
||||||
|
&input.note_ids,
|
||||||
|
&input.new_fields,
|
||||||
|
input.new_notetype_id,
|
||||||
|
usn,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrite notes to match new notetype, and assigns new notetype id.
|
||||||
|
///
|
||||||
|
/// `new_fields` should be the length of the new notetype's fields, and is a
|
||||||
|
/// list of the previous field index each field should be mapped to. If None,
|
||||||
|
/// the field is left empty.
|
||||||
|
fn update_notes_for_new_notetype_and_generate_cards(
|
||||||
|
&mut self,
|
||||||
|
note_ids: &[NoteId],
|
||||||
|
new_fields: &[Option<usize>],
|
||||||
|
new_notetype_id: NotetypeId,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<()> {
|
||||||
|
let notetype = self
|
||||||
|
.get_notetype(new_notetype_id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
let last_deck = self.get_last_deck_added_to_for_notetype(notetype.id);
|
||||||
|
let ctx = CardGenContext::new(¬etype, last_deck, usn);
|
||||||
|
|
||||||
|
for nid in note_ids {
|
||||||
|
let mut note = self.storage.get_note(*nid)?.unwrap();
|
||||||
|
let original = note.clone();
|
||||||
|
remap_fields(note.fields_mut(), new_fields);
|
||||||
|
note.notetype_id = new_notetype_id;
|
||||||
|
self.update_note_inner_generating_cards(
|
||||||
|
&ctx, &mut note, &original, true, false, false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cards_for_new_notetype(
|
||||||
|
&mut self,
|
||||||
|
note_ids: &[NoteId],
|
||||||
|
old_template_count: usize,
|
||||||
|
new_templates: Vec<Option<usize>>,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<()> {
|
||||||
|
let nids: Node = SearchNode::NoteIds(comma_separated_ids(note_ids)).into();
|
||||||
|
let map = TemplateMap::new(new_templates, old_template_count)?;
|
||||||
|
self.remove_unmapped_cards(&map, nids.clone(), usn)?;
|
||||||
|
self.rewrite_remapped_cards(&map, nids, usn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_unmapped_cards(
|
||||||
|
&mut self,
|
||||||
|
map: &TemplateMap,
|
||||||
|
nids: Node,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<(), AnkiError> {
|
||||||
|
if !map.removed.is_empty() {
|
||||||
|
let ords = Node::any(
|
||||||
|
map.removed
|
||||||
|
.iter()
|
||||||
|
.map(|o| TemplateKind::Ordinal(*o as u16))
|
||||||
|
.map(Into::into),
|
||||||
|
);
|
||||||
|
self.search_cards_into_table(match_all![nids, ords], SortMode::NoOrder)?;
|
||||||
|
for card in self.storage.all_searched_cards()? {
|
||||||
|
self.remove_card_and_add_grave_undoable(card, usn)?;
|
||||||
|
}
|
||||||
|
self.storage.clear_searched_cards_table()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_remapped_cards(
|
||||||
|
&mut self,
|
||||||
|
map: &TemplateMap,
|
||||||
|
nids: Node,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<(), AnkiError> {
|
||||||
|
if !map.remapped.is_empty() {
|
||||||
|
let ords = Node::any(
|
||||||
|
map.remapped
|
||||||
|
.keys()
|
||||||
|
.map(|o| TemplateKind::Ordinal(*o as u16))
|
||||||
|
.map(Into::into),
|
||||||
|
);
|
||||||
|
self.search_cards_into_table(match_all![nids, ords], SortMode::NoOrder)?;
|
||||||
|
for mut card in self.storage.all_searched_cards()? {
|
||||||
|
let original = card.clone();
|
||||||
|
card.template_idx =
|
||||||
|
*map.remapped.get(&(card.template_idx as usize)).unwrap() as u16;
|
||||||
|
self.update_card_inner(&mut card, original, usn)?;
|
||||||
|
}
|
||||||
|
self.storage.clear_searched_cards_table()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If provided notetype is a normal notetype, remove any card ordinals that
|
||||||
|
/// don't have a template associated with them. While recent Anki versions
|
||||||
|
/// should be able to handle this case, it can cause crashes on older
|
||||||
|
/// clients.
|
||||||
|
fn maybe_remove_cards_with_missing_template(
|
||||||
|
&mut self,
|
||||||
|
note_ids: &[NoteId],
|
||||||
|
notetype_id: NotetypeId,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<()> {
|
||||||
|
let notetype = self.get_notetype(notetype_id)?.ok_or(AnkiError::NotFound)?;
|
||||||
|
|
||||||
|
if notetype.config.kind() == NotetypeKind::Normal {
|
||||||
|
// cloze -> normal change requires clean up
|
||||||
|
for card in self
|
||||||
|
.storage
|
||||||
|
.all_cards_of_notes_above_ordinal(note_ids, notetype.templates.len() - 1)?
|
||||||
|
{
|
||||||
|
self.remove_card_and_add_grave_undoable(card, usn)?;
|
||||||
|
}
|
||||||
|
self.storage.clear_searched_notes_table()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrite the field list from a note to match a new notetype's fields.
|
||||||
|
fn remap_fields(fields: &mut Vec<String>, new_fields: &[Option<usize>]) {
|
||||||
|
*fields = new_fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
|
if let Some(idx) = *field {
|
||||||
|
// clone required as same field can be mapped multiple times
|
||||||
|
fields.get(idx).map(ToString::to_string).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::{collection::open_test_collection, error::Result};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_map() -> Result<()> {
|
||||||
|
let mut col = open_test_collection();
|
||||||
|
let mut basic = col
|
||||||
|
.storage
|
||||||
|
.get_notetype(col.get_current_notetype_id().unwrap())?
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// no matching field names; fields are assigned in order
|
||||||
|
let cloze = col.get_notetype_by_name("Cloze")?.unwrap().as_ref().clone();
|
||||||
|
assert_eq!(&default_field_map(&basic, &cloze), &[Some(0), Some(1)]);
|
||||||
|
|
||||||
|
basic.add_field("idx2");
|
||||||
|
basic.add_field("idx3");
|
||||||
|
basic.add_field("Text"); // 4
|
||||||
|
basic.add_field("idx5");
|
||||||
|
// re-fetch to get ordinals
|
||||||
|
col.update_notetype(&mut basic)?;
|
||||||
|
let basic = col.get_notetype(basic.id)?.unwrap();
|
||||||
|
|
||||||
|
// if names match, assignments are out of order; unmatched entries
|
||||||
|
// are filled sequentially
|
||||||
|
assert_eq!(&default_field_map(&basic, &cloze), &[Some(4), Some(0)]);
|
||||||
|
|
||||||
|
// unmatched entries are filled sequentially until exhausted
|
||||||
|
assert_eq!(
|
||||||
|
&default_field_map(&cloze, &basic),
|
||||||
|
&[
|
||||||
|
// front
|
||||||
|
Some(1),
|
||||||
|
// back
|
||||||
|
None,
|
||||||
|
// idx2
|
||||||
|
None,
|
||||||
|
// idx3
|
||||||
|
None,
|
||||||
|
// text
|
||||||
|
Some(0),
|
||||||
|
// idx5
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_map() -> Result<()> {
|
||||||
|
let new_templates = vec![None, Some(0)];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
TemplateMap::new(new_templates.clone(), 1)?,
|
||||||
|
TemplateMap {
|
||||||
|
removed: vec![],
|
||||||
|
remapped: vec![(0, 1)].into_iter().collect()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
TemplateMap::new(new_templates, 2)?,
|
||||||
|
TemplateMap {
|
||||||
|
removed: vec![1],
|
||||||
|
remapped: vec![(0, 1)].into_iter().collect()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic() -> Result<()> {
|
||||||
|
let mut col = open_test_collection();
|
||||||
|
let basic = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
|
let mut note = basic.new_note();
|
||||||
|
note.set_field(0, "1")?;
|
||||||
|
note.set_field(1, "2")?;
|
||||||
|
col.add_note(&mut note, DeckId(1))?;
|
||||||
|
|
||||||
|
let basic2 = col
|
||||||
|
.get_notetype_by_name("Basic (and reversed card)")?
|
||||||
|
.unwrap();
|
||||||
|
let mut info = col.notetype_change_info(vec![note.id], basic2.id)?;
|
||||||
|
|
||||||
|
// switch the existing card to ordinal 2
|
||||||
|
let first_card = col.storage.all_cards_of_note(note.id)?[0].clone();
|
||||||
|
assert_eq!(first_card.template_idx, 0);
|
||||||
|
let templates = info.input.new_templates.as_mut().unwrap();
|
||||||
|
*templates = vec![None, Some(0)];
|
||||||
|
col.change_notetype_of_notes(info.input)?;
|
||||||
|
|
||||||
|
// cards arrive in creation order, so the existing card will come first
|
||||||
|
let cards = col.storage.all_cards_of_note(note.id)?;
|
||||||
|
assert_eq!(cards[0].id, first_card.id);
|
||||||
|
assert_eq!(cards[0].template_idx, 1);
|
||||||
|
|
||||||
|
// a new forward card should also have been generated
|
||||||
|
assert_eq!(cards[1].template_idx, 0);
|
||||||
|
assert_ne!(cards[1].id, first_card.id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cloze() -> Result<()> {
|
||||||
|
let mut col = open_test_collection();
|
||||||
|
let basic = col
|
||||||
|
.get_notetype_by_name("Basic (and reversed card)")?
|
||||||
|
.unwrap();
|
||||||
|
let mut note = basic.new_note();
|
||||||
|
note.set_field(0, "1")?;
|
||||||
|
note.set_field(1, "2")?;
|
||||||
|
col.add_note(&mut note, DeckId(1))?;
|
||||||
|
|
||||||
|
let cloze = col.get_notetype_by_name("Cloze")?.unwrap();
|
||||||
|
|
||||||
|
// changing to cloze should leave all the existing cards alone
|
||||||
|
let info = col.notetype_change_info(vec![note.id], cloze.id)?;
|
||||||
|
col.change_notetype_of_notes(info.input)?;
|
||||||
|
let cards = col.storage.all_cards_of_note(note.id)?;
|
||||||
|
assert_eq!(cards.len(), 2);
|
||||||
|
|
||||||
|
// and back again should also work
|
||||||
|
let info = col.notetype_change_info(vec![note.id], basic.id)?;
|
||||||
|
col.change_notetype_of_notes(info.input)?;
|
||||||
|
let cards = col.storage.all_cards_of_note(note.id)?;
|
||||||
|
assert_eq!(cards.len(), 2);
|
||||||
|
|
||||||
|
// but any cards above the available templates should be removed when converting from cloze->normal
|
||||||
|
let info = col.notetype_change_info(vec![note.id], cloze.id)?;
|
||||||
|
col.change_notetype_of_notes(info.input)?;
|
||||||
|
|
||||||
|
let basic1 = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
|
let info = col.notetype_change_info(vec![note.id], basic1.id)?;
|
||||||
|
col.change_notetype_of_notes(info.input)?;
|
||||||
|
let cards = col.storage.all_cards_of_note(note.id)?;
|
||||||
|
assert_eq!(cards.len(), 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// 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
|
||||||
|
|
||||||
|
//! Updates to notes/cards when the structure of a notetype is changed.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::{CardGenContext, Notetype};
|
use super::{CardGenContext, Notetype};
|
||||||
|
@ -12,6 +12,7 @@ pub enum Op {
|
|||||||
AnswerCard,
|
AnswerCard,
|
||||||
BuildFilteredDeck,
|
BuildFilteredDeck,
|
||||||
Bury,
|
Bury,
|
||||||
|
ChangeNotetype,
|
||||||
ClearUnusedTags,
|
ClearUnusedTags,
|
||||||
EmptyFilteredDeck,
|
EmptyFilteredDeck,
|
||||||
ExpandCollapse,
|
ExpandCollapse,
|
||||||
@ -82,6 +83,7 @@ impl Op {
|
|||||||
Op::UpdateNotetype => tr.actions_update_notetype(),
|
Op::UpdateNotetype => tr.actions_update_notetype(),
|
||||||
Op::UpdateConfig => tr.actions_update_config(),
|
Op::UpdateConfig => tr.actions_update_config(),
|
||||||
Op::Custom(name) => name.into(),
|
Op::Custom(name) => name.into(),
|
||||||
|
Op::ChangeNotetype => tr.browsing_change_notetype(),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ mod test {
|
|||||||
// get the first card
|
// get the first card
|
||||||
let queued = col.get_next_card()?.unwrap();
|
let queued = col.get_next_card()?.unwrap();
|
||||||
let cid = queued.card.id;
|
let cid = queued.card.id;
|
||||||
let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1];
|
let sibling_cid = col.storage.all_card_ids_of_note_in_template_order(nid)?[1];
|
||||||
|
|
||||||
let assert_initial_state = |col: &mut Collection| -> Result<()> {
|
let assert_initial_state = |col: &mut Collection| -> Result<()> {
|
||||||
let first = col.storage.get_card(cid)?.unwrap();
|
let first = col.storage.get_card(cid)?.unwrap();
|
||||||
|
@ -344,7 +344,25 @@ impl super::SqliteStorage {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn all_card_ids_of_note_in_order(&self, nid: NoteId) -> Result<Vec<CardId>> {
|
pub(crate) fn all_cards_of_notes_above_ordinal(
|
||||||
|
&mut self,
|
||||||
|
note_ids: &[NoteId],
|
||||||
|
ordinal: usize,
|
||||||
|
) -> Result<Vec<Card>> {
|
||||||
|
self.set_search_table_to_note_ids(note_ids)?;
|
||||||
|
self.db
|
||||||
|
.prepare_cached(concat!(
|
||||||
|
include_str!("get_card.sql"),
|
||||||
|
" where nid in (select nid from search_nids) and ord > ?"
|
||||||
|
))?
|
||||||
|
.query_and_then(&[ordinal as i64], |r| row_to_card(r).map_err(Into::into))?
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn all_card_ids_of_note_in_template_order(
|
||||||
|
&self,
|
||||||
|
nid: NoteId,
|
||||||
|
) -> Result<Vec<CardId>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select id from cards where nid = ? order by ord")?
|
.prepare_cached("select id from cards where nid = ? order by ord")?
|
||||||
.query_and_then(&[nid], |r| Ok(CardId(r.get(0)?)))?
|
.query_and_then(&[nid], |r| Ok(CardId(r.get(0)?)))?
|
||||||
|
@ -21,19 +21,37 @@ use std::fmt::Write;
|
|||||||
pub(crate) use sqlite::SqliteStorage;
|
pub(crate) use sqlite::SqliteStorage;
|
||||||
pub(crate) use sync::open_and_check_sqlite_file;
|
pub(crate) use sync::open_and_check_sqlite_file;
|
||||||
|
|
||||||
// Write a list of IDs as '(x,y,...)' into the provided string.
|
/// Write a list of IDs as '(x,y,...)' into the provided string.
|
||||||
pub(crate) fn ids_to_string<T>(buf: &mut String, ids: &[T])
|
pub(crate) fn ids_to_string<T>(buf: &mut String, ids: &[T])
|
||||||
where
|
where
|
||||||
T: std::fmt::Display,
|
T: std::fmt::Display,
|
||||||
{
|
{
|
||||||
buf.push('(');
|
buf.push('(');
|
||||||
|
write_comma_separated_ids(buf, ids);
|
||||||
|
buf.push(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a list of Ids as 'x,y,...' into the provided string.
|
||||||
|
pub(crate) fn write_comma_separated_ids<T>(buf: &mut String, ids: &[T])
|
||||||
|
where
|
||||||
|
T: std::fmt::Display,
|
||||||
|
{
|
||||||
if !ids.is_empty() {
|
if !ids.is_empty() {
|
||||||
for id in ids.iter().skip(1) {
|
for id in ids.iter().skip(1) {
|
||||||
write!(buf, "{},", id).unwrap();
|
write!(buf, "{},", id).unwrap();
|
||||||
}
|
}
|
||||||
write!(buf, "{}", ids[0]).unwrap();
|
write!(buf, "{}", ids[0]).unwrap();
|
||||||
}
|
}
|
||||||
buf.push(')');
|
}
|
||||||
|
|
||||||
|
pub(crate) fn comma_separated_ids<T>(ids: &[T]) -> String
|
||||||
|
where
|
||||||
|
T: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let mut buf = String::new();
|
||||||
|
write_comma_separated_ids(&mut buf, ids);
|
||||||
|
|
||||||
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -225,7 +225,7 @@ impl super::SqliteStorage {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_searched_notes_table(&self) -> Result<()> {
|
pub(crate) fn clear_searched_notes_table(&self) -> Result<()> {
|
||||||
self.db
|
self.db
|
||||||
.execute("drop table if exists search_nids", NO_PARAMS)?;
|
.execute("drop table if exists search_nids", NO_PARAMS)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -234,7 +234,8 @@ impl super::SqliteStorage {
|
|||||||
/// Injects the provided card IDs into the search_nids table, for
|
/// Injects the provided card IDs into the search_nids table, for
|
||||||
/// when ids have arrived outside of a search.
|
/// when ids have arrived outside of a search.
|
||||||
/// Clear with clear_searched_notes_table().
|
/// Clear with clear_searched_notes_table().
|
||||||
fn set_search_table_to_note_ids(&mut self, notes: &[NoteId]) -> Result<()> {
|
/// WARNING: the column name is nid, not id.
|
||||||
|
pub(crate) fn set_search_table_to_note_ids(&mut self, notes: &[NoteId]) -> Result<()> {
|
||||||
self.setup_searched_notes_table()?;
|
self.setup_searched_notes_table()?;
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.db
|
.db
|
||||||
|
Loading…
Reference in New Issue
Block a user