normal note types now generate a dummy card if required

In the cloze deletion case, we already created a dummy card 0 when
no cloze deletions were found. This change makes normal note types
behave the same way - if no cards would be generated, a dummy card
0 is added to allow the note to be added.

This also applies when modifying note types - it is now possible
to delete card templates even if some notes only use that template,
as a dummy card 0 will be generated for notes that end up with no
cards left.
This commit is contained in:
Damien Elmes 2020-04-21 16:50:34 +10:00
parent 5c3e5c672f
commit f86c2dc567
7 changed files with 75 additions and 54 deletions

View File

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

View File

@ -48,9 +48,6 @@ pub enum AnkiError {
#[fail(display = "Operation modifies schema, but schema not marked modified.")]
SchemaChange,
#[fail(display = "No cards generated.")]
NoCardsGenerated,
}
// error helpers

View File

@ -142,13 +142,8 @@ impl Collection {
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 note first, as we need the allocated ID for the cards
self.storage.add_note(note)?;
self.add_generated_cards(ctx, note.id, &cards)
self.generate_cards_for_new_note(ctx, note)
}
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
@ -177,12 +172,7 @@ impl Collection {
#[cfg(test)]
mod test {
use super::{anki_base91, field_checksum};
use crate::{
collection::open_test_collection,
decks::DeckID,
err::{AnkiError, Result},
search::SortMode,
};
use crate::{collection::open_test_collection, decks::DeckID, err::Result};
#[test]
fn test_base91() {
@ -200,35 +190,43 @@ mod test {
}
#[test]
fn adding() -> Result<()> {
fn adding_cards() -> Result<()> {
let mut col = open_test_collection();
let nt = col.get_notetype_by_name("basic")?.unwrap();
let nt = col
.get_notetype_by_name("basic (and reversed card)")?
.unwrap();
let mut note = nt.new_note();
assert_eq!(col.add_note(&mut note), Err(AnkiError::NoCardsGenerated));
note.fields[1] = "foo".into();
assert_eq!(col.add_note(&mut note), Err(AnkiError::NoCardsGenerated));
note.fields[0] = "bar".into();
// if no cards are generated, 1 card is added
col.add_note(&mut note).unwrap();
let existing = col.storage.existing_cards_for_note(note.id)?;
assert_eq!(existing.len(), 1);
assert_eq!(existing[0].ord, 0);
assert_eq!(
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
.unwrap()
.len(),
1
);
// nothing changes if the first field is filled
note.fields[0] = "test".into();
col.update_note(&mut note).unwrap();
let existing = col.storage.existing_cards_for_note(note.id)?;
assert_eq!(existing.len(), 1);
assert_eq!(existing[0].ord, 0);
// second field causes another card to be generated
note.fields[1] = "test".into();
col.update_note(&mut note).unwrap();
let existing = col.storage.existing_cards_for_note(note.id)?;
assert_eq!(existing.len(), 2);
assert_eq!(existing[1].ord, 1);
// cloze cards also generate card 0 if no clozes are found
let nt = col.get_notetype_by_name("cloze")?.unwrap();
let mut note = nt.new_note();
// cloze cards without any cloze deletions are allowed
col.add_note(&mut note).unwrap();
let existing = col.storage.existing_cards_for_note(note.id)?;
assert_eq!(existing.len(), 1);
assert_eq!(existing[0].ord, 0);
assert_eq!(existing[0].original_deck_id, DeckID(1));
// and generate cards for any cloze deletions
note.fields[0] = "{{c1::foo}} {{c2::bar}} {{c3::baz}} {{c0::quux}} {{c501::over}}".into();
col.update_note(&mut note)?;
let existing = col.storage.existing_cards_for_note(note.id)?;

View File

@ -86,9 +86,20 @@ impl CardGenContext<'_> {
existing: &[AlreadyGeneratedCardInfo],
) -> Vec<CardToGenerate> {
let extracted = extract_data_from_existing_cards(existing);
match self.notetype.config.kind() {
let cards = match self.notetype.config.kind() {
NoteTypeKind::Normal => self.new_cards_required_normal(note, &extracted),
NoteTypeKind::Cloze => self.new_cards_required_cloze(note, &extracted),
};
if extracted.existing_ords.is_empty() && cards.is_empty() {
// if there are no existing cards and no cards will be generated,
// we add card 0 to ensure the note always has at least one card
vec![CardToGenerate {
ord: 0,
did: extracted.deck_id,
due: extracted.due,
}]
} else {
cards
}
}
@ -128,8 +139,7 @@ impl CardGenContext<'_> {
for field in note.fields() {
add_cloze_numbers_in_string(field, &mut set);
}
let cards: Vec<_> = set
.into_iter()
set.into_iter()
.filter_map(|cloze_ord| {
let card_ord = cloze_ord.saturating_sub(1).min(499);
if extracted.existing_ords.contains(&(card_ord as u32)) {
@ -142,17 +152,7 @@ impl CardGenContext<'_> {
})
}
})
.collect();
if cards.is_empty() && extracted.existing_ords.is_empty() {
// if no cloze deletions are found, we add a card with ord 0
vec![CardToGenerate {
ord: 0,
did: extracted.deck_id,
due: extracted.due,
}]
} else {
cards
}
.collect()
}
}
@ -200,12 +200,33 @@ pub(crate) fn extract_data_from_existing_cards(
}
impl Collection {
pub(crate) fn generate_cards_for_new_note(
&mut self,
ctx: &CardGenContext,
note: &Note,
) -> Result<()> {
self.generate_cards_for_note(ctx, note, false)
}
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)?;
self.generate_cards_for_note(ctx, note, true)
}
fn generate_cards_for_note(
&mut self,
ctx: &CardGenContext,
note: &Note,
check_existing: bool,
) -> Result<()> {
let existing = if check_existing {
self.storage.existing_cards_for_note(note.id)?
} else {
vec![]
};
let cards = ctx.new_cards_required(note, &existing);
if cards.is_empty() {
return Ok(());
@ -225,7 +246,7 @@ impl Collection {
continue;
}
let note = self.storage.get_note(nid)?.unwrap();
self.generate_cards_for_existing_note(ctx, &note)?;
self.generate_cards_for_note(ctx, &note, true)?;
}
Ok(())

View File

@ -166,12 +166,19 @@ impl NoteType {
self.templates.push(CardTemplate::new(name, qfmt, afmt));
}
pub(crate) fn prepare_for_adding(&mut self) {
pub(crate) fn prepare_for_adding(&mut self) -> Result<()> {
// defaults to 0
self.config.target_deck_id = 1;
if self.fields.is_empty() {
return Err(AnkiError::invalid_input("1 field required"));
}
if self.templates.is_empty() {
return Err(AnkiError::invalid_input("1 template required"));
}
self.normalize_names();
self.ensure_names_unique();
self.update_requirements();
Ok(())
}
pub fn new_note(&self) -> Note {

View File

@ -240,5 +240,4 @@ mod test {
}
}
// fixme: make sure we don't orphan notes

View File

@ -58,7 +58,7 @@ pub(crate) fn basic(i18n: &I18n) -> NoteType {
fieldref(back),
),
);
nt.prepare_for_adding();
nt.prepare_for_adding().unwrap();
nt
}
@ -74,7 +74,7 @@ pub(crate) fn basic_typing(i18n: &I18n) -> NoteType {
fieldref(front),
back
);
nt.prepare_for_adding();
nt.prepare_for_adding().unwrap();
nt
}
@ -92,7 +92,7 @@ pub(crate) fn basic_forward_reverse(i18n: &I18n) -> NoteType {
fieldref(front),
),
);
nt.prepare_for_adding();
nt.prepare_for_adding().unwrap();
nt
}
@ -103,7 +103,7 @@ pub(crate) fn basic_optional_reverse(i18n: &I18n) -> NoteType {
nt.add_field(addrev.as_ref());
let tmpl = &mut nt.templates[1].config;
tmpl.q_format = format!("{{{{#{}}}}}{}{{{{/{}}}}}", addrev, tmpl.q_format, addrev);
nt.prepare_for_adding();
nt.prepare_for_adding().unwrap();
nt
}
@ -124,6 +124,6 @@ pub(crate) fn cloze(i18n: &I18n) -> NoteType {
color: lightblue;
}
";
nt.prepare_for_adding();
nt.prepare_for_adding().unwrap();
nt
}