// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod addupdate; mod counts; mod current; mod filtered; pub(crate) mod limits; mod name; mod remove; mod reparent; mod schema11; mod stats; mod tree; pub(crate) mod undo; use std::sync::Arc; pub(crate) use counts::DueCounts; pub(crate) use name::immediate_parent_name; pub use name::NativeDeckName; pub use schema11::DeckSchema11; pub use crate::pb::decks::{ deck::{ filtered::{search_term::Order as FilteredSearchOrder, SearchTerm as FilteredSearchTerm}, kind_container::Kind as DeckKind, Common as DeckCommon, Filtered as FilteredDeck, KindContainer as DeckKindContainer, Normal as NormalDeck, }, Deck as DeckProto, }; use crate::{ define_newtype, error::FilteredDeckError, markdown::render_markdown, prelude::*, text::sanitize_html_no_images, }; define_newtype!(DeckId, i64); impl DeckId { pub(crate) fn or(self, other: DeckId) -> Self { if self.0 == 0 { other } else { self } } } #[derive(Debug, Clone, PartialEq)] pub struct Deck { pub id: DeckId, pub name: NativeDeckName, pub mtime_secs: TimestampSecs, pub usn: Usn, pub common: DeckCommon, pub kind: DeckKind, } impl Deck { pub fn new_normal() -> Deck { Deck { id: DeckId(0), name: NativeDeckName::from_native_str(""), mtime_secs: TimestampSecs(0), usn: Usn(0), common: DeckCommon { study_collapsed: true, browser_collapsed: true, ..Default::default() }, kind: DeckKind::Normal(NormalDeck { config_id: 1, // enable in the future // markdown_description = true, ..Default::default() }), } } /// 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 { Some(DeckConfigId(norm.config_id)) } else { None } } // used by tests at the moment #[allow(dead_code)] pub(crate) fn normal(&self) -> Result<&NormalDeck> { match &self.kind { DeckKind::Normal(normal) => Ok(normal), _ => invalid_input!("deck not normal"), } } #[allow(dead_code)] pub(crate) fn normal_mut(&mut self) -> Result<&mut NormalDeck> { match &mut self.kind { DeckKind::Normal(normal) => Ok(normal), _ => invalid_input!("deck not normal"), } } pub(crate) fn filtered(&self) -> Result<&FilteredDeck> { if let DeckKind::Filtered(filtered) = &self.kind { Ok(filtered) } else { Err(FilteredDeckError::FilteredDeckRequired.into()) } } #[allow(dead_code)] pub(crate) fn filtered_mut(&mut self) -> Result<&mut FilteredDeck> { if let DeckKind::Filtered(filtered) = &mut self.kind { Ok(filtered) } else { Err(FilteredDeckError::FilteredDeckRequired.into()) } } pub(crate) fn set_modified(&mut self, usn: Usn) { self.mtime_secs = TimestampSecs::now(); self.usn = usn; } pub fn rendered_description(&self) -> String { if let DeckKind::Normal(normal) = &self.kind { if normal.markdown_description { let description = render_markdown(&normal.description); // before allowing images, we'll need to handle relative image // links on the various platforms sanitize_html_no_images(&description) } else { String::new() } } else { String::new() } } } impl Collection { pub fn get_or_create_normal_deck(&mut self, human_name: &str) -> Result { let name = NativeDeckName::from_human_name(human_name); if let Some(did) = self.storage.get_deck_id(name.as_native_str())? { self.storage.get_deck(did).map(|opt| opt.unwrap()) } else { let mut deck = Deck::new_normal(); deck.name = name; self.add_or_update_deck(&mut deck)?; Ok(deck) } } } impl Collection { pub(crate) fn get_deck(&mut self, did: DeckId) -> Result>> { if let Some(deck) = self.state.deck_cache.get(&did) { return Ok(Some(deck.clone())); } if let Some(deck) = self.storage.get_deck(did)? { let deck = Arc::new(deck); self.state.deck_cache.insert(did, deck.clone()); Ok(Some(deck)) } else { Ok(None) } } pub(crate) fn default_deck_is_empty(&self) -> Result { self.storage.deck_is_empty(DeckId(1)) } /// 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> { self.storage .get_deck_id(NativeDeckName::from_human_name(human_name).as_native_str()) } } #[cfg(test)] mod test { use crate::{collection::open_test_collection, prelude::*, search::SortMode}; fn sorted_names(col: &Collection) -> Vec { col.storage .get_all_deck_names() .unwrap() .into_iter() .map(|d| d.1) .collect() } #[test] fn adding_updating() -> Result<()> { let mut col = open_test_collection(); let deck1 = col.get_or_create_normal_deck("foo")?; let deck2 = col.get_or_create_normal_deck("FOO")?; assert_eq!(deck1.id, deck2.id); assert_eq!(sorted_names(&col), vec!["Default", "foo"]); // missing parents should be automatically created, and case should match // existing parents let _deck3 = col.get_or_create_normal_deck("FOO::BAR::BAZ")?; assert_eq!( sorted_names(&col), vec!["Default", "foo", "foo::BAR", "foo::BAR::BAZ"] ); Ok(()) } #[test] fn renaming() -> Result<()> { let mut col = open_test_collection(); let _ = col.get_or_create_normal_deck("foo::bar::baz")?; let mut top_deck = col.get_or_create_normal_deck("foo")?; top_deck.name = NativeDeckName::from_native_str("other"); col.add_or_update_deck(&mut top_deck)?; assert_eq!( sorted_names(&col), vec!["Default", "other", "other::bar", "other::bar::baz"] ); // should do the right thing in the middle of the tree as well let mut middle = col.get_or_create_normal_deck("other::bar")?; middle.name = NativeDeckName::from_native_str("quux\x1ffoo"); col.add_or_update_deck(&mut middle)?; assert_eq!( sorted_names(&col), vec!["Default", "other", "quux", "quux::foo", "quux::foo::baz"] ); // add another child let _ = col.get_or_create_normal_deck("quux::foo::baz2"); // quux::foo -> quux::foo::baz::four // means quux::foo::baz2 should be quux::foo::baz::four::baz2 // and a new quux::foo should have been created middle.name = NativeDeckName::from_native_str("quux\x1ffoo\x1fbaz\x1ffour"); col.add_or_update_deck(&mut middle)?; assert_eq!( sorted_names(&col), vec![ "Default", "other", "quux", "quux::foo", "quux::foo::baz", "quux::foo::baz::four", "quux::foo::baz::four::baz", "quux::foo::baz::four::baz2" ] ); // should handle name conflicts middle.name = NativeDeckName::from_native_str("other"); col.add_or_update_deck(&mut middle)?; assert_eq!(middle.name.as_native_str(), "other+"); // public function takes human name col.rename_deck(middle.id, "one::two")?; assert_eq!( sorted_names(&col), vec![ "Default", "one", "one::two", "one::two::baz", "one::two::baz2", "other", "quux", "quux::foo", "quux::foo::baz", ] ); Ok(()) } #[test] fn default() -> Result<()> { // deleting the default deck will remove cards, but bring the deck back // as a top level deck let mut col = open_test_collection(); let mut default = col.get_or_create_normal_deck("default")?; default.name = NativeDeckName::from_native_str("one\x1ftwo"); col.add_or_update_deck(&mut default)?; // create a non-default deck confusingly named "default" let _fake_default = col.get_or_create_normal_deck("default")?; // add a card to the real default let nt = col.get_notetype_by_name("Basic")?.unwrap(); let mut note = nt.new_note(); col.add_note(&mut note, default.id)?; assert_ne!(col.search_cards("", SortMode::NoOrder)?, vec![]); // add a subdeck let _ = col.get_or_create_normal_deck("one::two::three")?; // delete top level let top = col.get_or_create_normal_deck("one")?; col.remove_decks_and_child_decks(&[top.id])?; // should have come back as "Default+" due to conflict assert_eq!(sorted_names(&col), vec!["default", "Default+"]); // and the cards it contained should have been removed assert_eq!(col.search_cards("", SortMode::NoOrder)?, vec![]); Ok(()) } }