From 9c3671cf3fa0dc78332e9ae90ba821693225a8b0 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 18:46:01 +0200 Subject: [PATCH 1/9] Create decks/name.rs --- rslib/src/decks/mod.rs | 220 +------------------------------------- rslib/src/decks/name.rs | 230 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 215 deletions(-) create mode 100644 rslib/src/decks/name.rs diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 24fd15f5b..b7602d170 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -4,6 +4,7 @@ mod counts; mod current; mod filtered; +mod name; mod schema11; mod tree; pub(crate) mod undo; @@ -24,12 +25,15 @@ use crate::{ error::{AnkiError, Result}, markdown::render_markdown, prelude::*, - text::normalize_to_nfc, text::sanitize_html_no_images, timestamp::TimestampSecs, types::Usn, }; pub(crate) use counts::DueCounts; +use name::normalize_native_name; +pub(crate) use name::{ + human_deck_name_to_native, immediate_parent_name, native_deck_name_to_human, reparented_name, +}; pub use schema11::DeckSchema11; use std::{borrow::Cow, sync::Arc}; @@ -123,10 +127,6 @@ impl Deck { } } - pub fn human_name(&self) -> String { - self.name.replace("\x1f", "::") - } - pub(crate) fn set_modified(&mut self, usn: Usn) { self.mtime_secs = TimestampSecs::now(); self.usn = usn; @@ -156,60 +156,6 @@ impl Deck { String::new() } } - - // Mutate name to human representation for sharing. - pub fn with_human_name(mut self) -> Self { - self.name = self.human_name(); - self - } -} - -fn invalid_char_for_deck_component(c: char) -> bool { - c.is_ascii_control() || c == '"' -} - -fn normalized_deck_name_component(comp: &str) -> Cow { - let mut out = normalize_to_nfc(comp); - if out.contains(invalid_char_for_deck_component) { - out = out.replace(invalid_char_for_deck_component, "").into(); - } - let trimmed = out.trim(); - if trimmed.is_empty() { - "blank".to_string().into() - } else if trimmed.len() != out.len() { - trimmed.to_string().into() - } else { - out - } -} - -fn normalize_native_name(name: &str) -> Cow { - if name - .split('\x1f') - .any(|comp| matches!(normalized_deck_name_component(comp), Cow::Owned(_))) - { - let comps: Vec<_> = name - .split('\x1f') - .map(normalized_deck_name_component) - .collect::>(); - comps.join("\x1f").into() - } else { - // no changes required - name.into() - } -} - -pub(crate) fn human_deck_name_to_native(name: &str) -> String { - let mut out = String::with_capacity(name.len()); - for comp in name.split("::") { - out.push_str(&normalized_deck_name_component(comp)); - out.push('\x1f'); - } - out.trim_end_matches('\x1f').into() -} - -pub(crate) fn native_deck_name_to_human(name: &str) -> String { - name.replace('\x1f', "::") } impl Collection { @@ -227,30 +173,6 @@ impl Collection { } } -pub(crate) fn immediate_parent_name(machine_name: &str) -> Option<&str> { - machine_name.rsplitn(2, '\x1f').nth(1) -} - -/// Determine name to rename a deck to, when `dragged` is dropped on `dropped`. -/// `dropped` being unset represents a drop at the top or bottom of the deck list. -/// The returned name should be used to rename `dragged`. -/// Arguments are expected in 'machine' form with an \x1f separator. -pub(crate) fn reparented_name(dragged: &str, dropped: Option<&str>) -> Option { - let dragged_base = dragged.rsplit('\x1f').next().unwrap(); - if let Some(dropped) = dropped { - if dropped.starts_with(dragged) { - // foo onto foo::bar, or foo onto itself -> no-op - None - } else { - // foo::bar onto baz -> baz::bar - Some(format!("{}\x1f{}", dropped, dragged_base)) - } - } else { - // foo::bar onto top level -> bar - Some(dragged_base.into()) - } -} - impl Collection { pub(crate) fn default_deck_is_empty(&self) -> Result { self.storage.deck_is_empty(DeckId(1)) @@ -301,15 +223,6 @@ impl Collection { }) } - pub fn rename_deck(&mut self, did: DeckId, new_human_name: &str) -> Result> { - self.transact(Op::RenameDeck, |col| { - let existing_deck = col.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; - let mut deck = existing_deck.clone(); - deck.name = human_deck_name_to_native(new_human_name); - col.update_deck_inner(&mut deck, existing_deck, col.usn()?) - }) - } - pub(crate) fn update_deck_inner( &mut self, deck: &mut Deck, @@ -346,22 +259,6 @@ impl Collection { self.add_or_update_deck_with_existing_id_undoable(deck) } - pub(crate) fn ensure_deck_name_unique(&self, deck: &mut Deck, usn: Usn) -> Result<()> { - loop { - match self.storage.get_deck_id(&deck.name)? { - Some(did) if did == deck.id => { - break; - } - None => break, - _ => (), - } - deck.name += "+"; - deck.set_modified(usn); - } - - Ok(()) - } - pub(crate) fn recover_missing_deck(&mut self, did: DeckId, usn: Usn) -> Result<()> { let mut deck = Deck::new_normal(); deck.id = did; @@ -382,23 +279,6 @@ impl Collection { } } - fn rename_child_decks(&mut self, old: &Deck, new_name: &str, usn: Usn) -> Result<()> { - let children = self.storage.child_decks(old)?; - let old_component_count = old.name.matches('\x1f').count() + 1; - - for mut child in children { - let original = child.clone(); - let child_components: Vec<_> = child.name.split('\x1f').collect(); - let child_only = &child_components[old_component_count..]; - let new_name = format!("{}\x1f{}", new_name, child_only.join("\x1f")); - child.name = new_name; - child.set_modified(usn); - self.update_single_deck_undoable(&mut child, original)?; - } - - Ok(()) - } - /// Add a single, normal deck with the provided name for a child deck. /// Caller must have done necessarily validation on name. fn add_parent_deck(&mut self, machine_name: &str, usn: Usn) -> Result<()> { @@ -523,31 +403,6 @@ impl Collection { Ok(cids.len()) } - pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result> { - if skip_empty_default && self.default_deck_is_empty()? { - Ok(self - .storage - .get_all_deck_names()? - .into_iter() - .filter(|(id, _name)| id.0 != 1) - .collect()) - } else { - self.storage.get_all_deck_names() - } - } - - pub fn get_all_normal_deck_names(&mut self) -> Result> { - Ok(self - .storage - .get_all_deck_names()? - .into_iter() - .filter(|(id, _name)| match self.get_deck(*id) { - Ok(Some(deck)) => !deck.is_filtered(), - _ => true, - }) - .collect()) - } - /// Apply input delta to deck, and its parents. /// Caller should ensure transaction. pub(crate) fn update_deck_stats( @@ -685,8 +540,6 @@ impl Collection { #[cfg(test)] mod test { - use super::{human_deck_name_to_native, immediate_parent_name, normalize_native_name}; - use crate::decks::reparented_name; use crate::{ collection::{open_test_collection, Collection}, error::Result, @@ -702,33 +555,6 @@ mod test { .collect() } - #[test] - fn parent() { - assert_eq!(immediate_parent_name("foo"), None); - assert_eq!(immediate_parent_name("foo\x1fbar"), Some("foo")); - assert_eq!( - immediate_parent_name("foo\x1fbar\x1fbaz"), - Some("foo\x1fbar") - ); - } - - #[test] - fn from_human() { - assert_eq!(&human_deck_name_to_native("foo"), "foo"); - assert_eq!(&human_deck_name_to_native("foo::bar"), "foo\x1fbar"); - assert_eq!(&human_deck_name_to_native("fo\x1fo::ba\nr"), "foo\x1fbar"); - assert_eq!( - &human_deck_name_to_native("foo::::baz"), - "foo\x1fblank\x1fbaz" - ); - } - - #[test] - fn normalize() { - assert_eq!(&normalize_native_name("foo\x1fbar"), "foo\x1fbar"); - assert_eq!(&normalize_native_name("fo\u{a}o\x1fbar"), "foo\x1fbar"); - } - #[test] fn adding_updating() -> Result<()> { let mut col = open_test_collection(); @@ -852,40 +678,4 @@ mod test { Ok(()) } - - #[test] - fn drag_drop() { - // use custom separator to make the tests easier to read - fn n(s: &str) -> String { - s.replace(":", "\x1f") - } - - #[allow(clippy::unnecessary_wraps)] - fn n_opt(s: &str) -> Option { - Some(n(s)) - } - - assert_eq!(reparented_name("drag", Some("drop")), n_opt("drop:drag")); - assert_eq!(reparented_name("drag", None), n_opt("drag")); - assert_eq!(reparented_name(&n("drag:child"), None), n_opt("child")); - assert_eq!( - reparented_name(&n("drag:child"), Some(&n("drop:deck"))), - n_opt("drop:deck:child") - ); - assert_eq!( - reparented_name(&n("drag:child"), Some("drag")), - n_opt("drag:child") - ); - assert_eq!( - reparented_name(&n("drag:child:grandchild"), Some("drag")), - n_opt("drag:grandchild") - ); - // drops to child not supported - assert_eq!( - reparented_name(&n("drag"), Some(&n("drag:child:grandchild"))), - None - ); - // name doesn't change when deck dropped on itself - assert_eq!(reparented_name(&n("foo:bar"), Some(&n("foo:bar"))), None); - } } diff --git a/rslib/src/decks/name.rs b/rslib/src/decks/name.rs new file mode 100644 index 000000000..ff0dc7294 --- /dev/null +++ b/rslib/src/decks/name.rs @@ -0,0 +1,230 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use crate::{prelude::*, text::normalize_to_nfc}; +use std::borrow::Cow; + +impl Deck { + pub fn human_name(&self) -> String { + self.name.replace("\x1f", "::") + } + + // Mutate name to human representation for sharing. + pub fn with_human_name(mut self) -> Self { + self.name = self.human_name(); + self + } +} + +impl Collection { + pub fn get_all_normal_deck_names(&mut self) -> Result> { + Ok(self + .storage + .get_all_deck_names()? + .into_iter() + .filter(|(id, _name)| match self.get_deck(*id) { + Ok(Some(deck)) => !deck.is_filtered(), + _ => true, + }) + .collect()) + } + + pub fn rename_deck(&mut self, did: DeckId, new_human_name: &str) -> Result> { + self.transact(Op::RenameDeck, |col| { + let existing_deck = col.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; + let mut deck = existing_deck.clone(); + deck.name = human_deck_name_to_native(new_human_name); + col.update_deck_inner(&mut deck, existing_deck, col.usn()?) + }) + } + + pub(super) fn rename_child_decks( + &mut self, + old: &Deck, + new_name: &str, + usn: Usn, + ) -> Result<()> { + let children = self.storage.child_decks(old)?; + let old_component_count = old.name.matches('\x1f').count() + 1; + + for mut child in children { + let original = child.clone(); + let child_components: Vec<_> = child.name.split('\x1f').collect(); + let child_only = &child_components[old_component_count..]; + let new_name = format!("{}\x1f{}", new_name, child_only.join("\x1f")); + child.name = new_name; + child.set_modified(usn); + self.update_single_deck_undoable(&mut child, original)?; + } + + Ok(()) + } + + pub(crate) fn ensure_deck_name_unique(&self, deck: &mut Deck, usn: Usn) -> Result<()> { + loop { + match self.storage.get_deck_id(&deck.name)? { + Some(did) if did == deck.id => { + break; + } + None => break, + _ => (), + } + deck.name += "+"; + deck.set_modified(usn); + } + + Ok(()) + } + + pub fn get_all_deck_names(&self, skip_empty_default: bool) -> Result> { + if skip_empty_default && self.default_deck_is_empty()? { + Ok(self + .storage + .get_all_deck_names()? + .into_iter() + .filter(|(id, _name)| id.0 != 1) + .collect()) + } else { + self.storage.get_all_deck_names() + } + } +} + +fn invalid_char_for_deck_component(c: char) -> bool { + c.is_ascii_control() || c == '"' +} + +fn normalized_deck_name_component(comp: &str) -> Cow { + let mut out = normalize_to_nfc(comp); + if out.contains(invalid_char_for_deck_component) { + out = out.replace(invalid_char_for_deck_component, "").into(); + } + let trimmed = out.trim(); + if trimmed.is_empty() { + "blank".to_string().into() + } else if trimmed.len() != out.len() { + trimmed.to_string().into() + } else { + out + } +} + +pub(super) fn normalize_native_name(name: &str) -> Cow { + if name + .split('\x1f') + .any(|comp| matches!(normalized_deck_name_component(comp), Cow::Owned(_))) + { + let comps: Vec<_> = name + .split('\x1f') + .map(normalized_deck_name_component) + .collect::>(); + comps.join("\x1f").into() + } else { + // no changes required + name.into() + } +} + +pub(crate) fn human_deck_name_to_native(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + for comp in name.split("::") { + out.push_str(&normalized_deck_name_component(comp)); + out.push('\x1f'); + } + out.trim_end_matches('\x1f').into() +} + +pub(crate) fn native_deck_name_to_human(name: &str) -> String { + name.replace('\x1f', "::") +} + +pub(crate) fn immediate_parent_name(machine_name: &str) -> Option<&str> { + machine_name.rsplitn(2, '\x1f').nth(1) +} + +/// Determine name to rename a deck to, when `dragged` is dropped on `dropped`. +/// `dropped` being unset represents a drop at the top or bottom of the deck list. +/// The returned name should be used to rename `dragged`. +/// Arguments are expected in 'machine' form with an \x1f separator. +pub(crate) fn reparented_name(dragged: &str, dropped: Option<&str>) -> Option { + let dragged_base = dragged.rsplit('\x1f').next().unwrap(); + if let Some(dropped) = dropped { + if dropped.starts_with(dragged) { + // foo onto foo::bar, or foo onto itself -> no-op + None + } else { + // foo::bar onto baz -> baz::bar + Some(format!("{}\x1f{}", dropped, dragged_base)) + } + } else { + // foo::bar onto top level -> bar + Some(dragged_base.into()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parent() { + assert_eq!(immediate_parent_name("foo"), None); + assert_eq!(immediate_parent_name("foo\x1fbar"), Some("foo")); + assert_eq!( + immediate_parent_name("foo\x1fbar\x1fbaz"), + Some("foo\x1fbar") + ); + } + + #[test] + fn from_human() { + assert_eq!(&human_deck_name_to_native("foo"), "foo"); + assert_eq!(&human_deck_name_to_native("foo::bar"), "foo\x1fbar"); + assert_eq!(&human_deck_name_to_native("fo\x1fo::ba\nr"), "foo\x1fbar"); + assert_eq!( + &human_deck_name_to_native("foo::::baz"), + "foo\x1fblank\x1fbaz" + ); + } + + #[test] + fn normalize() { + assert_eq!(&normalize_native_name("foo\x1fbar"), "foo\x1fbar"); + assert_eq!(&normalize_native_name("fo\u{a}o\x1fbar"), "foo\x1fbar"); + } + + #[test] + fn drag_drop() { + // use custom separator to make the tests easier to read + fn n(s: &str) -> String { + s.replace(":", "\x1f") + } + + #[allow(clippy::unnecessary_wraps)] + fn n_opt(s: &str) -> Option { + Some(n(s)) + } + + assert_eq!(reparented_name("drag", Some("drop")), n_opt("drop:drag")); + assert_eq!(reparented_name("drag", None), n_opt("drag")); + assert_eq!(reparented_name(&n("drag:child"), None), n_opt("child")); + assert_eq!( + reparented_name(&n("drag:child"), Some(&n("drop:deck"))), + n_opt("drop:deck:child") + ); + assert_eq!( + reparented_name(&n("drag:child"), Some("drag")), + n_opt("drag:child") + ); + assert_eq!( + reparented_name(&n("drag:child:grandchild"), Some("drag")), + n_opt("drag:grandchild") + ); + // drops to child not supported + assert_eq!( + reparented_name(&n("drag"), Some(&n("drag:child:grandchild"))), + None + ); + // name doesn't change when deck dropped on itself + assert_eq!(reparented_name(&n("foo:bar"), Some(&n("foo:bar"))), None); + } +} From 3f36db4f814a177782deaec1486b15f77048bec4 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 19:17:43 +0200 Subject: [PATCH 2/9] Create decks/reparent.rs --- rslib/src/decks/mod.rs | 56 ++--------------------------------- rslib/src/decks/reparent.rs | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 54 deletions(-) create mode 100644 rslib/src/decks/reparent.rs diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index b7602d170..2078eac78 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -5,6 +5,7 @@ mod counts; mod current; mod filtered; mod name; +mod reparent; mod schema11; mod tree; pub(crate) mod undo; @@ -32,7 +33,7 @@ use crate::{ pub(crate) use counts::DueCounts; use name::normalize_native_name; pub(crate) use name::{ - human_deck_name_to_native, immediate_parent_name, native_deck_name_to_human, reparented_name, + human_deck_name_to_native, immediate_parent_name, native_deck_name_to_human, }; pub use schema11::DeckSchema11; use std::{borrow::Cow, sync::Arc}; @@ -483,59 +484,6 @@ impl Collection { deck.set_modified(usn); self.update_single_deck_undoable(deck, original) } - - pub fn reparent_decks( - &mut self, - deck_ids: &[DeckId], - new_parent: Option, - ) -> Result> { - self.transact(Op::ReparentDeck, |col| { - col.reparent_decks_inner(deck_ids, new_parent) - }) - } - - pub fn reparent_decks_inner( - &mut self, - deck_ids: &[DeckId], - new_parent: Option, - ) -> Result { - let usn = self.usn()?; - let target_deck; - let mut target_name = None; - if let Some(target) = new_parent { - if let Some(target) = self.storage.get_deck(target)? { - if target.is_filtered() { - return Err(FilteredDeckError::MustBeLeafNode.into()); - } - target_deck = target; - target_name = Some(target_deck.name.as_str()); - } - } - - let mut count = 0; - for deck in deck_ids { - if let Some(mut deck) = self.storage.get_deck(*deck)? { - if let Some(new_name) = reparented_name(&deck.name, target_name) { - count += 1; - let orig = deck.clone(); - - // this is basically update_deck_inner(), except: - // - we skip the normalization in prepare_for_update() - // - we skip the match_or_create_parents() step - // - we skip the final create_missing_parents(), as we don't allow parent->child - // renames - - deck.set_modified(usn); - deck.name = new_name; - self.ensure_deck_name_unique(&mut deck, usn)?; - self.rename_child_decks(&orig, &deck.name, usn)?; - self.update_single_deck_undoable(&mut deck, orig)?; - } - } - } - - Ok(count) - } } #[cfg(test)] diff --git a/rslib/src/decks/reparent.rs b/rslib/src/decks/reparent.rs new file mode 100644 index 000000000..97186953c --- /dev/null +++ b/rslib/src/decks/reparent.rs @@ -0,0 +1,59 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use super::name::reparented_name; +use crate::{error::FilteredDeckError, prelude::*}; + +impl Collection { + pub fn reparent_decks( + &mut self, + deck_ids: &[DeckId], + new_parent: Option, + ) -> Result> { + self.transact(Op::ReparentDeck, |col| { + col.reparent_decks_inner(deck_ids, new_parent) + }) + } + + pub fn reparent_decks_inner( + &mut self, + deck_ids: &[DeckId], + new_parent: Option, + ) -> Result { + let usn = self.usn()?; + let target_deck; + let mut target_name = None; + if let Some(target) = new_parent { + if let Some(target) = self.storage.get_deck(target)? { + if target.is_filtered() { + return Err(FilteredDeckError::MustBeLeafNode.into()); + } + target_deck = target; + target_name = Some(target_deck.name.as_str()); + } + } + + let mut count = 0; + for deck in deck_ids { + if let Some(mut deck) = self.storage.get_deck(*deck)? { + if let Some(new_name) = reparented_name(&deck.name, target_name) { + count += 1; + let orig = deck.clone(); + + // this is basically update_deck_inner(), except: + // - we skip the normalization in prepare_for_update() + // - we skip the match_or_create_parents() step + // - we skip the final create_missing_parents(), as we don't allow parent->child + // renames + + deck.set_modified(usn); + deck.name = new_name; + self.ensure_deck_name_unique(&mut deck, usn)?; + self.rename_child_decks(&orig, &deck.name, usn)?; + self.update_single_deck_undoable(&mut deck, orig)?; + } + } + } + + Ok(count) + } +} From 7225b7e4dc764b9ad9307d68769c6ffa4f49ee5a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 19:29:52 +0200 Subject: [PATCH 3/9] Create decks/add.rs --- rslib/src/decks/add.rs | 163 +++++++++++++++++++++++++++++++++++++++++ rslib/src/decks/mod.rs | 160 +--------------------------------------- 2 files changed, 165 insertions(+), 158 deletions(-) create mode 100644 rslib/src/decks/add.rs diff --git a/rslib/src/decks/add.rs b/rslib/src/decks/add.rs new file mode 100644 index 000000000..7559c1675 --- /dev/null +++ b/rslib/src/decks/add.rs @@ -0,0 +1,163 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use super::name::{immediate_parent_name, normalize_native_name}; +use crate::{error::FilteredDeckError, prelude::*}; +use std::borrow::Cow; + +impl Collection { + /// Normalize deck name and rename if not unique. Bumps mtime and usn if + /// name was changed, but otherwise leaves it the same. + pub(super) fn prepare_deck_for_update(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { + if let Cow::Owned(name) = normalize_native_name(&deck.name) { + deck.name = name; + deck.set_modified(usn); + } + self.ensure_deck_name_unique(deck, usn) + } + + /// Add or update an existing deck modified by the user. May add parents, + /// or rename children as required. Prefer add_deck() or update_deck() to + /// be explicit about your intentions; this function mainly exists so we + /// can integrate with older Python code that behaved this way. + pub(crate) fn add_or_update_deck(&mut self, deck: &mut Deck) -> Result> { + if deck.id.0 == 0 { + self.add_deck(deck) + } else { + self.update_deck(deck) + } + } + + /// Add a new deck. The id must be 0, as it will be automatically assigned. + pub fn add_deck(&mut self, deck: &mut Deck) -> Result> { + if deck.id.0 != 0 { + return Err(AnkiError::invalid_input("deck to add must have id 0")); + } + + self.transact(Op::AddDeck, |col| col.add_deck_inner(deck, col.usn()?)) + } + + pub(crate) fn add_deck_inner(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { + self.prepare_deck_for_update(deck, usn)?; + deck.set_modified(usn); + self.match_or_create_parents(deck, usn)?; + self.add_deck_undoable(deck) + } + + pub fn update_deck(&mut self, deck: &mut Deck) -> Result> { + self.transact(Op::UpdateDeck, |col| { + let existing_deck = col.storage.get_deck(deck.id)?.ok_or(AnkiError::NotFound)?; + col.update_deck_inner(deck, existing_deck, col.usn()?) + }) + } + + pub(crate) fn update_deck_inner( + &mut self, + deck: &mut Deck, + original: Deck, + usn: Usn, + ) -> Result<()> { + self.prepare_deck_for_update(deck, usn)?; + deck.set_modified(usn); + let name_changed = original.name != deck.name; + if name_changed { + // match closest parent name + self.match_or_create_parents(deck, usn)?; + // rename children + self.rename_child_decks(&original, &deck.name, usn)?; + } + self.update_single_deck_undoable(deck, original)?; + if name_changed { + // after updating, we need to ensure all grandparents exist, which may not be the case + // in the parent->child case + self.create_missing_parents(&deck.name, usn)?; + } + Ok(()) + } + + /// Add/update a single deck when syncing/importing. Ensures name is unique + /// & normalized, but does not check parents/children or update mtime + /// (unless the name was changed). Caller must set up transaction. + pub(crate) fn add_or_update_single_deck_with_existing_id( + &mut self, + deck: &mut Deck, + usn: Usn, + ) -> Result<()> { + self.prepare_deck_for_update(deck, usn)?; + self.add_or_update_deck_with_existing_id_undoable(deck) + } + + pub(crate) fn recover_missing_deck(&mut self, did: DeckId, usn: Usn) -> Result<()> { + let mut deck = Deck::new_normal(); + deck.id = did; + deck.name = format!("recovered{}", did); + deck.set_modified(usn); + self.add_or_update_single_deck_with_existing_id(&mut deck, usn) + } + + /// Add a single, normal deck with the provided name for a child deck. + /// Caller must have done necessarily validation on name. + fn add_parent_deck(&mut self, machine_name: &str, usn: Usn) -> Result<()> { + let mut deck = Deck::new_normal(); + deck.name = machine_name.into(); + deck.set_modified(usn); + self.add_deck_undoable(&mut deck) + } + + /// If parent deck(s) exist, rewrite name to match their case. + /// If they don't exist, create them. + /// Returns an error if a DB operation fails, or if the first existing parent is a filtered deck. + fn match_or_create_parents(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { + let child_split: Vec<_> = deck.name.split('\x1f').collect(); + if let Some(parent_deck) = self.first_existing_parent(&deck.name, 0)? { + if parent_deck.is_filtered() { + return Err(FilteredDeckError::MustBeLeafNode.into()); + } + let parent_count = parent_deck.name.matches('\x1f').count() + 1; + let need_create = parent_count != child_split.len() - 1; + deck.name = format!( + "{}\x1f{}", + parent_deck.name, + &child_split[parent_count..].join("\x1f") + ); + if need_create { + self.create_missing_parents(&deck.name, usn)?; + } + Ok(()) + } else if child_split.len() == 1 { + // no parents required + Ok(()) + } else { + // no existing parents + self.create_missing_parents(&deck.name, usn) + } + } + + fn create_missing_parents(&mut self, mut machine_name: &str, usn: Usn) -> Result<()> { + while let Some(parent_name) = immediate_parent_name(machine_name) { + if self.storage.get_deck_id(parent_name)?.is_none() { + self.add_parent_deck(parent_name, usn)?; + } + machine_name = parent_name; + } + Ok(()) + } + + fn first_existing_parent( + &self, + machine_name: &str, + recursion_level: usize, + ) -> Result> { + if recursion_level > 10 { + return Err(AnkiError::invalid_input("deck nesting level too deep")); + } + if let Some(parent_name) = immediate_parent_name(machine_name) { + if let Some(parent_did) = self.storage.get_deck_id(parent_name)? { + self.storage.get_deck(parent_did) + } else { + self.first_existing_parent(parent_name, recursion_level + 1) + } + } else { + Ok(None) + } + } +} diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 2078eac78..86b549b6a 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod add; mod counts; mod current; mod filtered; @@ -31,12 +32,11 @@ use crate::{ types::Usn, }; pub(crate) use counts::DueCounts; -use name::normalize_native_name; pub(crate) use name::{ human_deck_name_to_native, immediate_parent_name, native_deck_name_to_human, }; pub use schema11::DeckSchema11; -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; define_newtype!(DeckId, i64); @@ -179,95 +179,6 @@ impl Collection { self.storage.deck_is_empty(DeckId(1)) } - /// Normalize deck name and rename if not unique. Bumps mtime and usn if - /// name was changed, but otherwise leaves it the same. - fn prepare_deck_for_update(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { - if let Cow::Owned(name) = normalize_native_name(&deck.name) { - deck.name = name; - deck.set_modified(usn); - } - self.ensure_deck_name_unique(deck, usn) - } - - /// Add or update an existing deck modified by the user. May add parents, - /// or rename children as required. Prefer add_deck() or update_deck() to - /// be explicit about your intentions; this function mainly exists so we - /// can integrate with older Python code that behaved this way. - pub(crate) fn add_or_update_deck(&mut self, deck: &mut Deck) -> Result> { - if deck.id.0 == 0 { - self.add_deck(deck) - } else { - self.update_deck(deck) - } - } - - /// Add a new deck. The id must be 0, as it will be automatically assigned. - pub fn add_deck(&mut self, deck: &mut Deck) -> Result> { - if deck.id.0 != 0 { - return Err(AnkiError::invalid_input("deck to add must have id 0")); - } - - self.transact(Op::AddDeck, |col| col.add_deck_inner(deck, col.usn()?)) - } - - pub(crate) fn add_deck_inner(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { - self.prepare_deck_for_update(deck, usn)?; - deck.set_modified(usn); - self.match_or_create_parents(deck, usn)?; - self.add_deck_undoable(deck) - } - - pub fn update_deck(&mut self, deck: &mut Deck) -> Result> { - self.transact(Op::UpdateDeck, |col| { - let existing_deck = col.storage.get_deck(deck.id)?.ok_or(AnkiError::NotFound)?; - col.update_deck_inner(deck, existing_deck, col.usn()?) - }) - } - - pub(crate) fn update_deck_inner( - &mut self, - deck: &mut Deck, - original: Deck, - usn: Usn, - ) -> Result<()> { - self.prepare_deck_for_update(deck, usn)?; - deck.set_modified(usn); - let name_changed = original.name != deck.name; - if name_changed { - // match closest parent name - self.match_or_create_parents(deck, usn)?; - // rename children - self.rename_child_decks(&original, &deck.name, usn)?; - } - self.update_single_deck_undoable(deck, original)?; - if name_changed { - // after updating, we need to ensure all grandparents exist, which may not be the case - // in the parent->child case - self.create_missing_parents(&deck.name, usn)?; - } - Ok(()) - } - - /// Add/update a single deck when syncing/importing. Ensures name is unique - /// & normalized, but does not check parents/children or update mtime - /// (unless the name was changed). Caller must set up transaction. - pub(crate) fn add_or_update_single_deck_with_existing_id( - &mut self, - deck: &mut Deck, - usn: Usn, - ) -> Result<()> { - self.prepare_deck_for_update(deck, usn)?; - self.add_or_update_deck_with_existing_id_undoable(deck) - } - - pub(crate) fn recover_missing_deck(&mut self, did: DeckId, usn: Usn) -> Result<()> { - let mut deck = Deck::new_normal(); - deck.id = did; - deck.name = format!("recovered{}", did); - deck.set_modified(usn); - self.add_or_update_single_deck_with_existing_id(&mut deck, usn) - } - pub fn get_or_create_normal_deck(&mut self, human_name: &str) -> Result { let native_name = human_deck_name_to_native(human_name); if let Some(did) = self.storage.get_deck_id(&native_name)? { @@ -280,73 +191,6 @@ impl Collection { } } - /// Add a single, normal deck with the provided name for a child deck. - /// Caller must have done necessarily validation on name. - fn add_parent_deck(&mut self, machine_name: &str, usn: Usn) -> Result<()> { - let mut deck = Deck::new_normal(); - deck.name = machine_name.into(); - deck.set_modified(usn); - self.add_deck_undoable(&mut deck) - } - - /// If parent deck(s) exist, rewrite name to match their case. - /// If they don't exist, create them. - /// Returns an error if a DB operation fails, or if the first existing parent is a filtered deck. - fn match_or_create_parents(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> { - let child_split: Vec<_> = deck.name.split('\x1f').collect(); - if let Some(parent_deck) = self.first_existing_parent(&deck.name, 0)? { - if parent_deck.is_filtered() { - return Err(FilteredDeckError::MustBeLeafNode.into()); - } - let parent_count = parent_deck.name.matches('\x1f').count() + 1; - let need_create = parent_count != child_split.len() - 1; - deck.name = format!( - "{}\x1f{}", - parent_deck.name, - &child_split[parent_count..].join("\x1f") - ); - if need_create { - self.create_missing_parents(&deck.name, usn)?; - } - Ok(()) - } else if child_split.len() == 1 { - // no parents required - Ok(()) - } else { - // no existing parents - self.create_missing_parents(&deck.name, usn) - } - } - - fn create_missing_parents(&mut self, mut machine_name: &str, usn: Usn) -> Result<()> { - while let Some(parent_name) = immediate_parent_name(machine_name) { - if self.storage.get_deck_id(parent_name)?.is_none() { - self.add_parent_deck(parent_name, usn)?; - } - machine_name = parent_name; - } - Ok(()) - } - - fn first_existing_parent( - &self, - machine_name: &str, - recursion_level: usize, - ) -> Result> { - if recursion_level > 10 { - return Err(AnkiError::invalid_input("deck nesting level too deep")); - } - if let Some(parent_name) = immediate_parent_name(machine_name) { - if let Some(parent_did) = self.storage.get_deck_id(parent_name)? { - self.storage.get_deck(parent_did) - } else { - self.first_existing_parent(parent_name, recursion_level + 1) - } - } else { - Ok(None) - } - } - /// Get a deck based on its human name. If you have a machine name, /// use the method in storage instead. pub(crate) fn get_deck_id(&self, human_name: &str) -> Result> { From a1617760d4678d75a9bb4094b58b54d273a29710 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 19:35:54 +0200 Subject: [PATCH 4/9] Create decks/remove.rs --- rslib/src/decks/mod.rs | 51 +----------------------------------- rslib/src/decks/remove.rs | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 50 deletions(-) create mode 100644 rslib/src/decks/remove.rs diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 86b549b6a..f2aeeaa2b 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -6,6 +6,7 @@ mod counts; mod current; mod filtered; mod name; +mod remove; mod reparent; mod schema11; mod tree; @@ -198,56 +199,6 @@ impl Collection { self.storage.get_deck_id(&machine_name) } - pub fn remove_decks_and_child_decks(&mut self, dids: &[DeckId]) -> Result> { - self.transact(Op::RemoveDeck, |col| { - let mut card_count = 0; - let usn = col.usn()?; - for did in dids { - if let Some(deck) = col.storage.get_deck(*did)? { - let child_decks = col.storage.child_decks(&deck)?; - - // top level - card_count += col.remove_single_deck(&deck, usn)?; - - // remove children - for deck in child_decks { - card_count += col.remove_single_deck(&deck, usn)?; - } - } - } - Ok(card_count) - }) - } - - pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result { - let card_count = match deck.kind { - DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?, - DeckKind::Filtered(_) => { - self.return_all_cards_in_filtered_deck(deck.id)?; - 0 - } - }; - self.clear_aux_config_for_deck(deck.id)?; - if deck.id.0 == 1 { - // if the default deck is included, just ensure it's reset to the default - // name, as we've already removed its cards - let mut modified_default = deck.clone(); - modified_default.name = self.tr.deck_config_default_name().into(); - self.prepare_deck_for_update(&mut modified_default, usn)?; - modified_default.set_modified(usn); - self.update_single_deck_undoable(&mut modified_default, deck.clone())?; - } else { - self.remove_deck_and_add_grave_undoable(deck.clone(), usn)?; - } - Ok(card_count) - } - - fn delete_all_cards_in_normal_deck(&mut self, did: DeckId) -> Result { - let cids = self.storage.all_cards_in_single_deck(did)?; - self.remove_cards_and_orphaned_notes(&cids)?; - Ok(cids.len()) - } - /// Apply input delta to deck, and its parents. /// Caller should ensure transaction. pub(crate) fn update_deck_stats( diff --git a/rslib/src/decks/remove.rs b/rslib/src/decks/remove.rs new file mode 100644 index 000000000..b28ea7c3f --- /dev/null +++ b/rslib/src/decks/remove.rs @@ -0,0 +1,55 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use crate::prelude::*; + +impl Collection { + pub fn remove_decks_and_child_decks(&mut self, dids: &[DeckId]) -> Result> { + self.transact(Op::RemoveDeck, |col| { + let mut card_count = 0; + let usn = col.usn()?; + for did in dids { + if let Some(deck) = col.storage.get_deck(*did)? { + let child_decks = col.storage.child_decks(&deck)?; + + // top level + card_count += col.remove_single_deck(&deck, usn)?; + + // remove children + for deck in child_decks { + card_count += col.remove_single_deck(&deck, usn)?; + } + } + } + Ok(card_count) + }) + } + + pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result { + let card_count = match deck.kind { + DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?, + DeckKind::Filtered(_) => { + self.return_all_cards_in_filtered_deck(deck.id)?; + 0 + } + }; + self.clear_aux_config_for_deck(deck.id)?; + if deck.id.0 == 1 { + // if the default deck is included, just ensure it's reset to the default + // name, as we've already removed its cards + let mut modified_default = deck.clone(); + modified_default.name = self.tr.deck_config_default_name().into(); + self.prepare_deck_for_update(&mut modified_default, usn)?; + modified_default.set_modified(usn); + self.update_single_deck_undoable(&mut modified_default, deck.clone())?; + } else { + self.remove_deck_and_add_grave_undoable(deck.clone(), usn)?; + } + Ok(card_count) + } + + fn delete_all_cards_in_normal_deck(&mut self, did: DeckId) -> Result { + let cids = self.storage.all_cards_in_single_deck(did)?; + self.remove_cards_and_orphaned_notes(&cids)?; + Ok(cids.len()) + } +} From 9dac8e2b5f69f1267509badd18393f5021a8b2bf Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 19:43:35 +0200 Subject: [PATCH 5/9] Tidy up blocks and imports in decks/mod.rs --- rslib/src/decks/mod.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index f2aeeaa2b..c90ac488f 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -20,17 +20,8 @@ pub use crate::backend_proto::{ Deck as DeckProto, }; use crate::{ - backend_proto as pb, - collection::Collection, - deckconf::DeckConfId, - define_newtype, - error::FilteredDeckError, - error::{AnkiError, Result}, - markdown::render_markdown, - prelude::*, - text::sanitize_html_no_images, - timestamp::TimestampSecs, - types::Usn, + backend_proto as pb, define_newtype, error::FilteredDeckError, markdown::render_markdown, + prelude::*, text::sanitize_html_no_images, }; pub(crate) use counts::DueCounts; pub(crate) use name::{ @@ -173,9 +164,7 @@ impl Collection { Ok(None) } } -} -impl Collection { pub(crate) fn default_deck_is_empty(&self) -> Result { self.storage.deck_is_empty(DeckId(1)) } From d2337e4cd36c72337b20336eebf2a45ca7ecde36 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 19:53:11 +0200 Subject: [PATCH 6/9] Move some methods into decks/counts.rs --- rslib/src/decks/counts.rs | 30 ++++++++++++++++++++++++++++-- rslib/src/decks/mod.rs | 23 ----------------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/rslib/src/decks/counts.rs b/rslib/src/decks/counts.rs index 26f547864..436107b34 100644 --- a/rslib/src/decks/counts.rs +++ b/rslib/src/decks/counts.rs @@ -1,7 +1,8 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -use crate::{collection::Collection, decks::DeckId, error::Result}; +use crate::{ + backend_proto as pb, collection::Collection, decks::DeckId, error::Result, prelude::*, +}; use std::collections::HashMap; #[derive(Debug)] @@ -11,6 +12,18 @@ pub(crate) struct DueCounts { pub learning: u32, } +impl Deck { + /// Return the studied counts if studied today. + /// May be negative if user has extended limits. + pub(crate) fn new_rev_counts(&self, today: u32) -> (i32, i32) { + if self.common.last_day_studied == today { + (self.common.new_studied, self.common.review_studied) + } else { + (0, 0) + } + } +} + impl Collection { /// Get due counts for decks at the given timestamp. pub(crate) fn due_counts( @@ -26,4 +39,17 @@ impl Collection { limit_to, ) } + + pub(crate) fn counts_for_deck_today( + &mut self, + did: DeckId, + ) -> Result { + let today = self.current_due_day(0)?; + let mut deck = self.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; + deck.reset_stats_if_day_changed(today); + Ok(pb::CountsForDeckTodayOut { + new: deck.common.new_studied, + review: deck.common.review_studied, + }) + } } diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index c90ac488f..3330cb83b 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -125,16 +125,6 @@ impl Deck { self.usn = usn; } - /// Return the studied counts if studied today. - /// May be negative if user has extended limits. - pub(crate) fn new_rev_counts(&self, today: u32) -> (i32, i32) { - if self.common.last_day_studied == today { - (self.common.new_studied, self.common.review_studied) - } else { - (0, 0) - } - } - pub fn rendered_description(&self) -> String { if let DeckKind::Normal(normal) = &self.kind { if normal.markdown_description { @@ -239,19 +229,6 @@ impl Collection { Ok(()) } - pub(crate) fn counts_for_deck_today( - &mut self, - did: DeckId, - ) -> Result { - let today = self.current_due_day(0)?; - let mut deck = self.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; - deck.reset_stats_if_day_changed(today); - Ok(pb::CountsForDeckTodayOut { - new: deck.common.new_studied, - review: deck.common.review_studied, - }) - } - fn update_deck_stats_single( &mut self, today: u32, From 3138fccacae852dd2e83ff2066aec8526b8fe22d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 20:06:16 +0200 Subject: [PATCH 7/9] Create decks/stats.rs --- rslib/src/decks/mod.rs | 84 ++------------------------------------ rslib/src/decks/stats.rs | 87 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 rslib/src/decks/stats.rs diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 3330cb83b..557ab0b39 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -9,6 +9,7 @@ mod name; mod remove; mod reparent; mod schema11; +mod stats; mod tree; pub(crate) mod undo; @@ -20,8 +21,8 @@ pub use crate::backend_proto::{ Deck as DeckProto, }; use crate::{ - backend_proto as pb, define_newtype, error::FilteredDeckError, markdown::render_markdown, - prelude::*, text::sanitize_html_no_images, + define_newtype, error::FilteredDeckError, markdown::render_markdown, prelude::*, + text::sanitize_html_no_images, }; pub(crate) use counts::DueCounts; pub(crate) use name::{ @@ -63,17 +64,6 @@ impl Deck { } } - fn reset_stats_if_day_changed(&mut self, today: u32) { - let c = &mut self.common; - if c.last_day_studied != today { - c.new_studied = 0; - c.learning_studied = 0; - c.review_studied = 0; - c.milliseconds_studied = 0; - c.last_day_studied = today; - } - } - /// Returns deck config ID if deck is a normal deck. pub(crate) fn config_id(&self) -> Option { if let DeckKind::Normal(ref norm) = self.kind { @@ -177,74 +167,6 @@ impl Collection { let machine_name = human_deck_name_to_native(&human_name); self.storage.get_deck_id(&machine_name) } - - /// Apply input delta to deck, and its parents. - /// Caller should ensure transaction. - pub(crate) fn update_deck_stats( - &mut self, - today: u32, - usn: Usn, - input: pb::UpdateStatsIn, - ) -> Result<()> { - let did = input.deck_id.into(); - let mutator = |c: &mut DeckCommon| { - c.new_studied += input.new_delta; - c.review_studied += input.review_delta; - c.milliseconds_studied += input.millisecond_delta; - }; - if let Some(mut deck) = self.storage.get_deck(did)? { - self.update_deck_stats_single(today, usn, &mut deck, mutator)?; - for mut deck in self.storage.parent_decks(&deck)? { - self.update_deck_stats_single(today, usn, &mut deck, mutator)?; - } - } - Ok(()) - } - - /// Modify the deck's limits by adjusting the 'done today' count. - /// Positive values increase the limit, negative value decrease it. - /// Caller should ensure a transaction. - pub(crate) fn extend_limits( - &mut self, - today: u32, - usn: Usn, - did: DeckId, - new_delta: i32, - review_delta: i32, - ) -> Result<()> { - let mutator = |c: &mut DeckCommon| { - c.new_studied -= new_delta; - c.review_studied -= review_delta; - }; - if let Some(mut deck) = self.storage.get_deck(did)? { - self.update_deck_stats_single(today, usn, &mut deck, mutator)?; - for mut deck in self.storage.parent_decks(&deck)? { - self.update_deck_stats_single(today, usn, &mut deck, mutator)?; - } - for mut deck in self.storage.child_decks(&deck)? { - self.update_deck_stats_single(today, usn, &mut deck, mutator)?; - } - } - - Ok(()) - } - - fn update_deck_stats_single( - &mut self, - today: u32, - usn: Usn, - deck: &mut Deck, - mutator: F, - ) -> Result<()> - where - F: FnOnce(&mut DeckCommon), - { - let original = deck.clone(); - deck.reset_stats_if_day_changed(today); - mutator(&mut deck.common); - deck.set_modified(usn); - self.update_single_deck_undoable(deck, original) - } } #[cfg(test)] diff --git a/rslib/src/decks/stats.rs b/rslib/src/decks/stats.rs new file mode 100644 index 000000000..5eda8b0c9 --- /dev/null +++ b/rslib/src/decks/stats.rs @@ -0,0 +1,87 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use super::DeckCommon; +use crate::{backend_proto as pb, prelude::*}; + +impl Deck { + pub(super) fn reset_stats_if_day_changed(&mut self, today: u32) { + let c = &mut self.common; + if c.last_day_studied != today { + c.new_studied = 0; + c.learning_studied = 0; + c.review_studied = 0; + c.milliseconds_studied = 0; + c.last_day_studied = today; + } + } +} + +impl Collection { + /// Apply input delta to deck, and its parents. + /// Caller should ensure transaction. + pub(crate) fn update_deck_stats( + &mut self, + today: u32, + usn: Usn, + input: pb::UpdateStatsIn, + ) -> Result<()> { + let did = input.deck_id.into(); + let mutator = |c: &mut DeckCommon| { + c.new_studied += input.new_delta; + c.review_studied += input.review_delta; + c.milliseconds_studied += input.millisecond_delta; + }; + if let Some(mut deck) = self.storage.get_deck(did)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + for mut deck in self.storage.parent_decks(&deck)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + } + } + Ok(()) + } + + /// Modify the deck's limits by adjusting the 'done today' count. + /// Positive values increase the limit, negative value decrease it. + /// Caller should ensure a transaction. + pub(crate) fn extend_limits( + &mut self, + today: u32, + usn: Usn, + did: DeckId, + new_delta: i32, + review_delta: i32, + ) -> Result<()> { + let mutator = |c: &mut DeckCommon| { + c.new_studied -= new_delta; + c.review_studied -= review_delta; + }; + if let Some(mut deck) = self.storage.get_deck(did)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + for mut deck in self.storage.parent_decks(&deck)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + } + for mut deck in self.storage.child_decks(&deck)? { + self.update_deck_stats_single(today, usn, &mut deck, mutator)?; + } + } + + Ok(()) + } + + fn update_deck_stats_single( + &mut self, + today: u32, + usn: Usn, + deck: &mut Deck, + mutator: F, + ) -> Result<()> + where + F: FnOnce(&mut DeckCommon), + { + let original = deck.clone(); + deck.reset_stats_if_day_changed(today); + mutator(&mut deck.common); + deck.set_modified(usn); + self.update_single_deck_undoable(deck, original) + } +} From 5931631d76fb8e9fe9f2f7772b8828a6ca6fc9d5 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 15 Apr 2021 20:07:16 +0200 Subject: [PATCH 8/9] Remove some imports in decks which are in prelude --- rslib/src/decks/counts.rs | 4 +--- rslib/src/decks/filtered.rs | 2 +- rslib/src/decks/schema11.rs | 5 ++--- rslib/src/decks/tree.rs | 12 ++---------- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/rslib/src/decks/counts.rs b/rslib/src/decks/counts.rs index 436107b34..5507e33fd 100644 --- a/rslib/src/decks/counts.rs +++ b/rslib/src/decks/counts.rs @@ -1,8 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::{ - backend_proto as pb, collection::Collection, decks::DeckId, error::Result, prelude::*, -}; +use crate::{backend_proto as pb, prelude::*}; use std::collections::HashMap; #[derive(Debug)] diff --git a/rslib/src/decks/filtered.rs b/rslib/src/decks/filtered.rs index 88498aae8..84c065dca 100644 --- a/rslib/src/decks/filtered.rs +++ b/rslib/src/decks/filtered.rs @@ -3,7 +3,7 @@ use strum::IntoEnumIterator; -use super::{DeckCommon, DeckKind, FilteredDeck, FilteredSearchOrder, FilteredSearchTerm}; +use super::{DeckCommon, FilteredDeck, FilteredSearchOrder, FilteredSearchTerm}; use crate::prelude::*; impl Deck { diff --git a/rslib/src/decks/schema11.rs b/rslib/src/decks/schema11.rs index 2b86630a6..bf08625b0 100644 --- a/rslib/src/decks/schema11.rs +++ b/rslib/src/decks/schema11.rs @@ -3,13 +3,12 @@ use super::DeckId; use super::{ - human_deck_name_to_native, native_deck_name_to_human, Deck, DeckCommon, DeckKind, FilteredDeck, + human_deck_name_to_native, native_deck_name_to_human, DeckCommon, FilteredDeck, FilteredSearchTerm, NormalDeck, }; use crate::{ + prelude::*, serde::{default_on_invalid, deserialize_bool_from_anything, deserialize_number_from_string}, - timestamp::TimestampSecs, - types::Usn, }; use serde_derive::{Deserialize, Serialize}; diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index d4fa199e6..4c580819c 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -1,18 +1,10 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::{Deck, DeckKind, DueCounts}; +use super::DueCounts; pub use crate::backend_proto::set_deck_collapsed_in::Scope as DeckCollapseScope; use crate::{ - backend_proto::DeckTreeNode, - collection::Collection, - config::{BoolKey, SchedulerVersion}, - deckconf::{DeckConf, DeckConfId}, - decks::DeckId, - error::Result, - ops::OpOutput, - timestamp::TimestampSecs, - undo::Op, + backend_proto::DeckTreeNode, config::SchedulerVersion, ops::OpOutput, prelude::*, undo::Op, }; use serde_tuple::Serialize_tuple; use std::{ From c03acf832bfb141ef63f96776cc83d0a01cbb2b4 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Fri, 16 Apr 2021 08:30:16 +0200 Subject: [PATCH 9/9] Split Col impls in decks in pub and private blocks --- rslib/src/decks/add.rs | 2 ++ rslib/src/decks/remove.rs | 2 ++ rslib/src/decks/stats.rs | 2 ++ rslib/src/decks/undo.rs | 26 ++++++++++++++------------ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/rslib/src/decks/add.rs b/rslib/src/decks/add.rs index 7559c1675..bf30c9c35 100644 --- a/rslib/src/decks/add.rs +++ b/rslib/src/decks/add.rs @@ -93,7 +93,9 @@ impl Collection { deck.set_modified(usn); self.add_or_update_single_deck_with_existing_id(&mut deck, usn) } +} +impl Collection { /// Add a single, normal deck with the provided name for a child deck. /// Caller must have done necessarily validation on name. fn add_parent_deck(&mut self, machine_name: &str, usn: Usn) -> Result<()> { diff --git a/rslib/src/decks/remove.rs b/rslib/src/decks/remove.rs index b28ea7c3f..997100dfa 100644 --- a/rslib/src/decks/remove.rs +++ b/rslib/src/decks/remove.rs @@ -46,7 +46,9 @@ impl Collection { } Ok(card_count) } +} +impl Collection { fn delete_all_cards_in_normal_deck(&mut self, did: DeckId) -> Result { let cids = self.storage.all_cards_in_single_deck(did)?; self.remove_cards_and_orphaned_notes(&cids)?; diff --git a/rslib/src/decks/stats.rs b/rslib/src/decks/stats.rs index 5eda8b0c9..b69bccca6 100644 --- a/rslib/src/decks/stats.rs +++ b/rslib/src/decks/stats.rs @@ -67,7 +67,9 @@ impl Collection { Ok(()) } +} +impl Collection { fn update_deck_stats_single( &mut self, today: u32, diff --git a/rslib/src/decks/undo.rs b/rslib/src/decks/undo.rs index 39cdfaf14..5bbdb81f7 100644 --- a/rslib/src/decks/undo.rs +++ b/rslib/src/decks/undo.rs @@ -30,6 +30,20 @@ impl Collection { } } + pub(crate) fn remove_deck_and_add_grave_undoable( + &mut self, + deck: Deck, + usn: Usn, + ) -> Result<()> { + self.state.deck_cache.clear(); + self.add_deck_grave_undoable(deck.id, usn)?; + self.storage.remove_deck(deck.id)?; + self.save_undo(UndoableDeckChange::Removed(Box::new(deck))); + Ok(()) + } +} + +impl Collection { pub(super) fn add_deck_undoable(&mut self, deck: &mut Deck) -> Result<(), AnkiError> { self.storage.add_deck(deck)?; self.save_undo(UndoableDeckChange::Added(Box::new(deck.clone()))); @@ -59,18 +73,6 @@ impl Collection { self.storage.update_deck(deck) } - pub(crate) fn remove_deck_and_add_grave_undoable( - &mut self, - deck: Deck, - usn: Usn, - ) -> Result<()> { - self.state.deck_cache.clear(); - self.add_deck_grave_undoable(deck.id, usn)?; - self.storage.remove_deck(deck.id)?; - self.save_undo(UndoableDeckChange::Removed(Box::new(deck))); - Ok(()) - } - fn restore_deleted_deck(&mut self, deck: Deck) -> Result<()> { self.storage.add_or_update_deck_with_existing_id(&deck)?; self.save_undo(UndoableDeckChange::Added(Box::new(deck)));