template changes and card generation
Cloze cards are not yet supported, missing decks are not handled, and more testing is still required.
This commit is contained in:
parent
c34e3e8096
commit
83bcb084fe
@ -45,6 +45,7 @@ unicase = "=2.6.0"
|
||||
futures = "0.3.4"
|
||||
rand = "0.7.3"
|
||||
num-integer = "0.1.42"
|
||||
itertools = "0.9.0"
|
||||
|
||||
# pinned until rusqlite 0.22 comes out
|
||||
[target.'cfg(target_vendor="apple")'.dependencies.rusqlite]
|
||||
|
@ -100,11 +100,12 @@ impl Undoable for UpdateCardUndo {
|
||||
}
|
||||
|
||||
impl Card {
|
||||
pub fn new(nid: NoteID, ord: u16, deck_id: DeckID) -> Self {
|
||||
pub fn new(nid: NoteID, ord: u16, deck_id: DeckID, due: i32) -> Self {
|
||||
let mut card = Card::default();
|
||||
card.nid = nid;
|
||||
card.ord = ord;
|
||||
card.did = deck_id;
|
||||
card.due = due;
|
||||
card
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ pub(crate) enum ConfigKey {
|
||||
Rollover,
|
||||
LocalOffset,
|
||||
CurrentNoteTypeID,
|
||||
NextNewCardPosition,
|
||||
}
|
||||
|
||||
impl From<ConfigKey> for &'static str {
|
||||
@ -51,6 +52,7 @@ impl From<ConfigKey> for &'static str {
|
||||
ConfigKey::Rollover => "rollover",
|
||||
ConfigKey::LocalOffset => "localOffset",
|
||||
ConfigKey::CurrentNoteTypeID => "curModel",
|
||||
ConfigKey::NextNewCardPosition => "nextPos",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,6 +134,14 @@ impl Collection {
|
||||
pub(crate) fn set_current_notetype_id(&self, id: NoteTypeID) -> Result<()> {
|
||||
self.set_config(ConfigKey::CurrentNoteTypeID, &id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_and_update_next_card_position(&self) -> Result<u32> {
|
||||
let pos: u32 = self
|
||||
.get_config_optional(ConfigKey::NextNewCardPosition)
|
||||
.unwrap_or_default();
|
||||
self.set_config(ConfigKey::NextNewCardPosition, &pos.wrapping_add(1))?;
|
||||
Ok(pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, PartialEq, Debug)]
|
||||
|
@ -2,7 +2,6 @@
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{
|
||||
card::Card,
|
||||
collection::Collection,
|
||||
define_newtype,
|
||||
err::{AnkiError, Result},
|
||||
@ -134,36 +133,42 @@ impl Collection {
|
||||
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
|
||||
self.transact(None, |col| {
|
||||
let nt = col
|
||||
.storage
|
||||
.get_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)
|
||||
let ctx = CardGenContext::new(&nt, col.usn()?);
|
||||
col.add_note_inner(&ctx, note)
|
||||
})
|
||||
}
|
||||
|
||||
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());
|
||||
pub(crate) fn add_note_inner(&mut self, ctx: &CardGenContext, note: &mut Note) -> Result<()> {
|
||||
note.prepare_for_update(&ctx.notetype, ctx.usn)?;
|
||||
let cards = ctx.new_cards_required(note, Default::default());
|
||||
if cards.is_empty() {
|
||||
return Err(AnkiError::NoCardsGenerated);
|
||||
}
|
||||
|
||||
// add the note
|
||||
// add note first, as we need the allocated ID for the cards
|
||||
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)?;
|
||||
}
|
||||
self.add_generated_cards(ctx, note.id, &cards)
|
||||
}
|
||||
|
||||
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
||||
self.transact(None, |col| {
|
||||
let nt = col
|
||||
.get_notetype(note.ntid)?
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
||||
let ctx = CardGenContext::new(&nt, col.usn()?);
|
||||
col.update_note_inner(&ctx, note)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn update_note_inner(
|
||||
&mut self,
|
||||
ctx: &CardGenContext,
|
||||
note: &mut Note,
|
||||
) -> Result<()> {
|
||||
note.prepare_for_update(ctx.notetype, ctx.usn)?;
|
||||
self.generate_cards_for_existing_note(ctx, note)?;
|
||||
self.storage.update_note(note)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
211
rslib/src/notetype/cardgen.rs
Normal file
211
rslib/src/notetype/cardgen.rs
Normal file
@ -0,0 +1,211 @@
|
||||
// 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::{
|
||||
card::{Card, CardID},
|
||||
collection::Collection,
|
||||
decks::DeckID,
|
||||
err::Result,
|
||||
notes::{Note, NoteID},
|
||||
template::ParsedTemplate,
|
||||
types::Usn,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Info about an existing card required when generating new cards
|
||||
pub(crate) struct AlreadyGeneratedCardInfo {
|
||||
pub id: CardID,
|
||||
pub nid: NoteID,
|
||||
pub ord: u32,
|
||||
pub original_deck_id: DeckID,
|
||||
pub position_if_new: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CardToGenerate {
|
||||
pub ord: u32,
|
||||
pub did: Option<DeckID>,
|
||||
pub due: Option<u32>,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn is_nonempty(&self, card_ord: usize, nonempty_fields: &HashSet<&str>) -> bool {
|
||||
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 false;
|
||||
}
|
||||
};
|
||||
|
||||
template.renders_with_fields(&nonempty_fields)
|
||||
}
|
||||
|
||||
/// Returns the cards that need to be generated for the provided note.
|
||||
pub(crate) fn new_cards_required(
|
||||
&self,
|
||||
note: &Note,
|
||||
existing: &[AlreadyGeneratedCardInfo],
|
||||
) -> Vec<CardToGenerate> {
|
||||
let nonempty_fields = note.nonempty_fields(&self.notetype.fields);
|
||||
let extracted = extract_data_from_existing_cards(existing);
|
||||
self.new_cards_required_inner(&nonempty_fields, &extracted)
|
||||
}
|
||||
|
||||
fn new_cards_required_inner(
|
||||
&self,
|
||||
nonempty_fields: &HashSet<&str>,
|
||||
extracted: &ExtractedCardInfo,
|
||||
) -> Vec<CardToGenerate> {
|
||||
self.cards
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ord, card)| {
|
||||
if !extracted.existing_ords.contains(&(ord as u32))
|
||||
&& self.is_nonempty(ord, &nonempty_fields)
|
||||
{
|
||||
Some(CardToGenerate {
|
||||
ord: ord as u32,
|
||||
did: card.target_deck_id.or(extracted.deck_id),
|
||||
due: extracted.due,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// this could be reworked in the future to avoid the extra vec allocation
|
||||
fn group_generated_cards_by_note(
|
||||
items: Vec<AlreadyGeneratedCardInfo>,
|
||||
) -> Vec<(NoteID, Vec<AlreadyGeneratedCardInfo>)> {
|
||||
let mut out = vec![];
|
||||
for (key, group) in &items.into_iter().group_by(|c| c.nid) {
|
||||
out.push((key, group.collect()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub(crate) struct ExtractedCardInfo {
|
||||
// if set, the due position new cards should be given
|
||||
pub due: Option<u32>,
|
||||
// if set, the deck all current cards are in
|
||||
pub deck_id: Option<DeckID>,
|
||||
pub existing_ords: HashSet<u32>,
|
||||
}
|
||||
|
||||
pub(crate) fn extract_data_from_existing_cards(
|
||||
cards: &[AlreadyGeneratedCardInfo],
|
||||
) -> ExtractedCardInfo {
|
||||
let mut due = None;
|
||||
let mut deck_ids = HashSet::new();
|
||||
for card in cards {
|
||||
if due.is_none() && card.position_if_new.is_some() {
|
||||
due = card.position_if_new;
|
||||
}
|
||||
deck_ids.insert(card.original_deck_id);
|
||||
}
|
||||
let existing_ords: HashSet<_> = cards.iter().map(|c| c.ord).collect();
|
||||
ExtractedCardInfo {
|
||||
due,
|
||||
deck_id: if deck_ids.len() == 1 {
|
||||
deck_ids.into_iter().next()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
existing_ords,
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn generate_cards_for_existing_note(
|
||||
&mut self,
|
||||
ctx: &CardGenContext,
|
||||
note: &Note,
|
||||
) -> Result<()> {
|
||||
let existing = self.storage.existing_cards_for_note(note.id)?;
|
||||
let cards = ctx.new_cards_required(note, &existing);
|
||||
if cards.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
self.add_generated_cards(ctx, note.id, &cards)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_cards_for_notetype(&mut self, ctx: &CardGenContext) -> Result<()> {
|
||||
let existing_cards = self.storage.existing_cards_for_notetype(ctx.notetype.id)?;
|
||||
let by_note = group_generated_cards_by_note(existing_cards);
|
||||
for (nid, existing_cards) in by_note {
|
||||
if existing_cards.len() == ctx.notetype.templates.len() {
|
||||
// nothing to do
|
||||
continue;
|
||||
}
|
||||
let note = self.storage.get_note(nid)?.unwrap();
|
||||
self.generate_cards_for_existing_note(ctx, ¬e)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn add_generated_cards(
|
||||
&mut self,
|
||||
ctx: &CardGenContext,
|
||||
nid: NoteID,
|
||||
cards: &[CardToGenerate],
|
||||
) -> Result<()> {
|
||||
let mut next_pos = None;
|
||||
for c in cards {
|
||||
let did = c.did.unwrap_or_else(|| ctx.notetype.target_deck_id());
|
||||
let due = c.due.unwrap_or_else(|| {
|
||||
if next_pos.is_none() {
|
||||
next_pos = Some(self.get_and_update_next_card_position().unwrap_or(0));
|
||||
}
|
||||
next_pos.unwrap()
|
||||
});
|
||||
let mut card = Card::new(nid, c.ord as u16, did, due as i32);
|
||||
self.add_card(&mut card)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// fixme: deal with case where invalid deck pointed to
|
||||
// fixme: cloze cards, & avoid template count comparison for cloze
|
@ -1,85 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
}
|
@ -1,7 +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 cardgen;
|
||||
mod fields;
|
||||
mod schema11;
|
||||
mod schemachange;
|
||||
@ -13,7 +13,7 @@ pub use crate::backend_proto::{
|
||||
CardRequirement, CardTemplateConfig, NoteFieldConfig, NoteType as NoteTypeProto,
|
||||
NoteTypeConfig,
|
||||
};
|
||||
pub(crate) use cardgeninfo::CardGenContext;
|
||||
pub(crate) use cardgen::{AlreadyGeneratedCardInfo, CardGenContext};
|
||||
pub use fields::NoteField;
|
||||
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11};
|
||||
pub use stock::all_stock_notetypes;
|
||||
@ -199,11 +199,10 @@ impl Collection {
|
||||
pub fn update_notetype(&mut self, nt: &mut NoteType) -> Result<()> {
|
||||
self.transact(None, |col| {
|
||||
let existing_notetype = col
|
||||
.storage
|
||||
.get_notetype(nt.id)?
|
||||
.ok_or_else(|| AnkiError::invalid_input("no such notetype"))?;
|
||||
col.update_notes_for_changed_fields(nt, existing_notetype.fields.len())?;
|
||||
// fixme: card templates
|
||||
col.update_cards_for_changed_templates(nt, existing_notetype.templates.len())?;
|
||||
// fixme: update cache instead of clearing
|
||||
col.state.notetype_cache.remove(&nt.id);
|
||||
|
||||
|
@ -1,45 +1,71 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::{NoteField, NoteType};
|
||||
use super::{CardGenContext, NoteType};
|
||||
use crate::{collection::Collection, err::Result};
|
||||
|
||||
/// If any fields added, removed or reordered, returns a list of the new
|
||||
/// field length, comprised of the original ordinals.
|
||||
fn field_change_map(fields: &[NoteField], previous_field_count: usize) -> Option<Vec<Option<u32>>> {
|
||||
let map: Vec<_> = fields.iter().map(|f| f.ord).collect();
|
||||
let changed = map.len() != previous_field_count
|
||||
|| map
|
||||
/// True if any ordinals added, removed or reordered.
|
||||
fn ords_changed(ords: &[Option<u32>], previous_len: usize) -> bool {
|
||||
ords.len() != previous_len
|
||||
|| ords
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(idx, f)| f != &Some(idx as u32));
|
||||
if changed {
|
||||
Some(map)
|
||||
} else {
|
||||
None
|
||||
.any(|(idx, &ord)| ord != Some(idx as u32))
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Debug)]
|
||||
struct TemplateOrdChanges {
|
||||
added: Vec<u32>,
|
||||
removed: Vec<u32>,
|
||||
// map of old->new
|
||||
moved: Vec<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl TemplateOrdChanges {
|
||||
fn new(ords: Vec<Option<u32>>, previous_len: u32) -> Self {
|
||||
let mut changes = TemplateOrdChanges::default();
|
||||
let mut removed: Vec<_> = (0..previous_len).map(|v| Some(v as u32)).collect();
|
||||
for (idx, old_ord) in ords.into_iter().enumerate() {
|
||||
if let Some(old_ord) = old_ord {
|
||||
if let Some(entry) = removed.get_mut(old_ord as usize) {
|
||||
// guard required to ensure we don't panic if invalid high ordinal received
|
||||
*entry = None;
|
||||
}
|
||||
if old_ord == idx as u32 {
|
||||
// no action
|
||||
} else {
|
||||
changes.moved.push((old_ord as u32, idx as u32));
|
||||
}
|
||||
} else {
|
||||
changes.added.push(idx as u32);
|
||||
}
|
||||
}
|
||||
|
||||
changes.removed = removed.into_iter().filter_map(|v| v).collect();
|
||||
|
||||
changes
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
/// Caller must create transaction
|
||||
/// Rewrite notes to match the updated field schema.
|
||||
/// Caller must create transaction.
|
||||
pub(crate) fn update_notes_for_changed_fields(
|
||||
&mut self,
|
||||
nt: &NoteType,
|
||||
previous_field_count: usize,
|
||||
) -> Result<()> {
|
||||
let change_map = match field_change_map(&nt.fields, previous_field_count) {
|
||||
None => {
|
||||
// nothing to do
|
||||
return Ok(());
|
||||
}
|
||||
Some(map) => map,
|
||||
};
|
||||
let ords: Vec<_> = nt.fields.iter().map(|f| f.ord).collect();
|
||||
if !ords_changed(&ords, previous_field_count) {
|
||||
// nothing to do
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let nids = self.search_notes_only(&format!("mid:{}", nt.id))?;
|
||||
let usn = self.usn()?;
|
||||
for nid in nids {
|
||||
let mut note = self.storage.get_note(nid)?.unwrap();
|
||||
note.fields = change_map
|
||||
note.fields = ords
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if let Some(idx) = f {
|
||||
@ -58,12 +84,97 @@ impl Collection {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update cards after card templates added, removed or reordered.
|
||||
/// Does not remove cards where the template still exists but creates an empty card.
|
||||
/// Caller must create transaction.
|
||||
pub(crate) fn update_cards_for_changed_templates(
|
||||
&mut self,
|
||||
nt: &NoteType,
|
||||
previous_template_count: usize,
|
||||
) -> Result<()> {
|
||||
let ords: Vec<_> = nt.templates.iter().map(|f| f.ord).collect();
|
||||
if !ords_changed(&ords, previous_template_count) {
|
||||
// nothing to do
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let changes = TemplateOrdChanges::new(ords, previous_template_count as u32);
|
||||
if !changes.removed.is_empty() {
|
||||
self.storage
|
||||
.remove_cards_for_deleted_templates(nt.id, &changes.removed)?;
|
||||
}
|
||||
if !changes.moved.is_empty() {
|
||||
self.storage
|
||||
.move_cards_for_repositioned_templates(nt.id, &changes.moved)?;
|
||||
}
|
||||
|
||||
let ctx = CardGenContext::new(nt, self.usn()?);
|
||||
self.generate_cards_for_notetype(&ctx)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::collection::open_test_collection;
|
||||
use crate::err::Result;
|
||||
use super::{ords_changed, TemplateOrdChanges};
|
||||
use crate::{collection::open_test_collection, err::Result, search::SortMode};
|
||||
|
||||
#[test]
|
||||
fn ord_changes() {
|
||||
assert_eq!(ords_changed(&[Some(0), Some(1)], 2), false);
|
||||
assert_eq!(ords_changed(&[Some(0), Some(1)], 1), true);
|
||||
assert_eq!(ords_changed(&[Some(1), Some(0)], 2), true);
|
||||
assert_eq!(ords_changed(&[None, Some(1)], 2), true);
|
||||
assert_eq!(ords_changed(&[Some(0), Some(1), None], 2), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_changes() {
|
||||
assert_eq!(
|
||||
TemplateOrdChanges::new(vec![Some(0), Some(1)], 2),
|
||||
TemplateOrdChanges::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
TemplateOrdChanges::new(vec![Some(0), Some(1)], 3),
|
||||
TemplateOrdChanges {
|
||||
removed: vec![2],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
TemplateOrdChanges::new(vec![Some(1)], 2),
|
||||
TemplateOrdChanges {
|
||||
removed: vec![0],
|
||||
moved: vec![(1, 0)],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
TemplateOrdChanges::new(vec![Some(0), None], 1),
|
||||
TemplateOrdChanges {
|
||||
added: vec![1],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
TemplateOrdChanges::new(vec![Some(2), None, Some(0)], 2),
|
||||
TemplateOrdChanges {
|
||||
added: vec![1],
|
||||
moved: vec![(2, 0), (0, 2)],
|
||||
removed: vec![1],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
TemplateOrdChanges::new(vec![None, Some(2), None, Some(4)], 5),
|
||||
TemplateOrdChanges {
|
||||
added: vec![0, 2],
|
||||
moved: vec![(2, 1), (4, 3)],
|
||||
removed: vec![0, 1, 3],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fields() -> Result<()> {
|
||||
@ -94,4 +205,37 @@ mod test {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cards() -> Result<()> {
|
||||
let mut col = open_test_collection();
|
||||
let mut nt = col
|
||||
.storage
|
||||
.get_notetype(col.get_current_notetype_id().unwrap())?
|
||||
.unwrap();
|
||||
let mut note = nt.new_note();
|
||||
assert_eq!(note.fields.len(), 2);
|
||||
note.fields = vec!["one".into(), "two".into()];
|
||||
col.add_note(&mut note)?;
|
||||
|
||||
assert_eq!(
|
||||
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
// add an extra card template
|
||||
nt.add_template("card 2", "{{Front}}", "");
|
||||
col.update_notetype(&mut nt)?;
|
||||
|
||||
assert_eq!(
|
||||
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ use crate::notes::field_checksum;
|
||||
use crate::notetype::NoteTypeID;
|
||||
use crate::text::matches_wildcard;
|
||||
use crate::text::without_combining;
|
||||
use crate::{collection::Collection, text::strip_html_preserving_image_filenames};
|
||||
use crate::{
|
||||
collection::Collection, storage::ids_to_string, text::strip_html_preserving_image_filenames,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::{Captures, Regex};
|
||||
use std::fmt::Write;
|
||||
@ -373,21 +375,6 @@ impl SqlWriter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// Write a list of IDs as '(x,y,...)' into the provided string.
|
||||
fn ids_to_string<T>(buf: &mut String, ids: &[T])
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
buf.push('(');
|
||||
if !ids.is_empty() {
|
||||
for id in ids.iter().skip(1) {
|
||||
write!(buf, "{},", id).unwrap();
|
||||
}
|
||||
write!(buf, "{}", ids[0]).unwrap();
|
||||
}
|
||||
buf.push(')');
|
||||
}
|
||||
|
||||
/// Convert a string with _, % or * characters into a regex.
|
||||
fn glob_to_re(glob: &str) -> Option<String> {
|
||||
if !glob.contains(|c| c == '_' || c == '*' || c == '%') {
|
||||
@ -426,7 +413,6 @@ fn glob_to_re(glob: &str) -> Option<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ids_to_string;
|
||||
use crate::{
|
||||
collection::{open_collection, Collection},
|
||||
i18n::I18n,
|
||||
@ -436,23 +422,6 @@ mod test {
|
||||
use std::{fs, path::PathBuf};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn ids_string() {
|
||||
let mut s = String::new();
|
||||
ids_to_string::<u8>(&mut s, &[]);
|
||||
assert_eq!(s, "()");
|
||||
s.clear();
|
||||
ids_to_string(&mut s, &[7]);
|
||||
assert_eq!(s, "(7)");
|
||||
s.clear();
|
||||
ids_to_string(&mut s, &[7, 6]);
|
||||
assert_eq!(s, "(6,7)");
|
||||
s.clear();
|
||||
ids_to_string(&mut s, &[7, 6, 5]);
|
||||
assert_eq!(s, "(6,5,7)");
|
||||
s.clear();
|
||||
}
|
||||
|
||||
use super::super::parser::parse;
|
||||
use super::*;
|
||||
|
||||
|
@ -12,3 +12,42 @@ mod tag;
|
||||
mod upgrades;
|
||||
|
||||
pub(crate) use sqlite::SqliteStorage;
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
// Write a list of IDs as '(x,y,...)' into the provided string.
|
||||
pub(crate) fn ids_to_string<T>(buf: &mut String, ids: &[T])
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
buf.push('(');
|
||||
if !ids.is_empty() {
|
||||
for id in ids.iter().skip(1) {
|
||||
write!(buf, "{},", id).unwrap();
|
||||
}
|
||||
write!(buf, "{}", ids[0]).unwrap();
|
||||
}
|
||||
buf.push(')');
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ids_to_string;
|
||||
|
||||
#[test]
|
||||
fn ids_string() {
|
||||
let mut s = String::new();
|
||||
ids_to_string::<u8>(&mut s, &[]);
|
||||
assert_eq!(s, "()");
|
||||
s.clear();
|
||||
ids_to_string(&mut s, &[7]);
|
||||
assert_eq!(s, "(7)");
|
||||
s.clear();
|
||||
ids_to_string(&mut s, &[7, 6]);
|
||||
assert_eq!(s, "(6,7)");
|
||||
s.clear();
|
||||
ids_to_string(&mut s, &[7, 6, 5]);
|
||||
assert_eq!(s, "(6,5,7)");
|
||||
s.clear();
|
||||
}
|
||||
}
|
||||
|
10
rslib/src/storage/notetype/delete_cards_for_template.sql
Normal file
10
rslib/src/storage/notetype/delete_cards_for_template.sql
Normal file
@ -0,0 +1,10 @@
|
||||
delete from cards
|
||||
where
|
||||
nid in (
|
||||
select
|
||||
id
|
||||
from notes
|
||||
where
|
||||
mid = ?
|
||||
)
|
||||
and ord = ?;
|
27
rslib/src/storage/notetype/existing_cards.sql
Normal file
27
rslib/src/storage/notetype/existing_cards.sql
Normal file
@ -0,0 +1,27 @@
|
||||
select
|
||||
id,
|
||||
nid,
|
||||
ord,
|
||||
-- original deck
|
||||
(
|
||||
case
|
||||
odid
|
||||
when 0 then did
|
||||
else odid
|
||||
end
|
||||
),
|
||||
-- new position if card is empty
|
||||
(
|
||||
case
|
||||
type
|
||||
when 0 then (
|
||||
case
|
||||
odue
|
||||
when 0 then due
|
||||
else odue
|
||||
end
|
||||
)
|
||||
else null
|
||||
end
|
||||
)
|
||||
from cards c
|
@ -1,10 +1,14 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::SqliteStorage;
|
||||
use super::{ids_to_string, SqliteStorage};
|
||||
use crate::{
|
||||
err::{AnkiError, DBErrorKind, Result},
|
||||
notetype::{CardTemplate, CardTemplateConfig, NoteField, NoteFieldConfig, NoteTypeConfig},
|
||||
notes::NoteID,
|
||||
notetype::{
|
||||
AlreadyGeneratedCardInfo, CardTemplate, CardTemplateConfig, NoteField, NoteFieldConfig,
|
||||
NoteTypeConfig,
|
||||
},
|
||||
notetype::{NoteType, NoteTypeID, NoteTypeSchema11},
|
||||
timestamp::TimestampMillis,
|
||||
};
|
||||
@ -26,6 +30,16 @@ fn row_to_notetype_core(row: &Row) -> Result<NoteType> {
|
||||
})
|
||||
}
|
||||
|
||||
fn row_to_existing_card(row: &Row) -> Result<AlreadyGeneratedCardInfo> {
|
||||
Ok(AlreadyGeneratedCardInfo {
|
||||
id: row.get(0)?,
|
||||
nid: row.get(1)?,
|
||||
ord: row.get(2)?,
|
||||
original_deck_id: row.get(3)?,
|
||||
position_if_new: row.get(4)?,
|
||||
})
|
||||
}
|
||||
|
||||
impl SqliteStorage {
|
||||
pub(crate) fn get_notetype(&self, ntid: NoteTypeID) -> Result<Option<NoteType>> {
|
||||
match self.get_notetype_core(ntid)? {
|
||||
@ -165,6 +179,69 @@ impl SqliteStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_cards_for_deleted_templates(
|
||||
&self,
|
||||
ntid: NoteTypeID,
|
||||
ords: &[u32],
|
||||
) -> Result<()> {
|
||||
let mut stmt = self
|
||||
.db
|
||||
.prepare(include_str!("delete_cards_for_template.sql"))?;
|
||||
for ord in ords {
|
||||
stmt.execute(params![ntid, ord])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_cards_for_repositioned_templates(
|
||||
&self,
|
||||
ntid: NoteTypeID,
|
||||
changes: &[(u32, u32)],
|
||||
) -> Result<()> {
|
||||
let case_clauses: Vec<_> = changes
|
||||
.iter()
|
||||
.map(|(old, new)| format!("when {} then {}", old, new))
|
||||
.collect();
|
||||
let mut sql = format!(
|
||||
"update cards set ord = (case ord {} end)
|
||||
where nid in (select id from notes where mid = ?)
|
||||
and ord in ",
|
||||
case_clauses.join(" ")
|
||||
);
|
||||
ids_to_string(
|
||||
&mut sql,
|
||||
&changes.iter().map(|(old, _)| old).collect::<Vec<_>>(),
|
||||
);
|
||||
self.db.prepare(&sql)?.execute(&[ntid])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn existing_cards_for_notetype(
|
||||
&self,
|
||||
ntid: NoteTypeID,
|
||||
) -> Result<Vec<AlreadyGeneratedCardInfo>> {
|
||||
self.db
|
||||
.prepare_cached(concat!(
|
||||
include_str!("existing_cards.sql"),
|
||||
" where c.nid in (select id from notes where mid=?)"
|
||||
))?
|
||||
.query_and_then(&[ntid], row_to_existing_card)?
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn existing_cards_for_note(
|
||||
&self,
|
||||
nid: NoteID,
|
||||
) -> Result<Vec<AlreadyGeneratedCardInfo>> {
|
||||
self.db
|
||||
.prepare_cached(concat!(
|
||||
include_str!("existing_cards.sql"),
|
||||
" where c.nid = ?"
|
||||
))?
|
||||
.query_and_then(&[nid], row_to_existing_card)?
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Upgrading/downgrading/legacy
|
||||
|
||||
pub(crate) fn get_all_notetypes_as_schema11(
|
||||
|
Loading…
Reference in New Issue
Block a user