use std::convert::TryFrom; use std::io::Write; use std::str::FromStr; use clap::ArgMatches; use chrono::{DateTime, Utc}; use regex::Regex; use crate::error::{Result, Error}; use crate::database::Database; use crate::formatters::Formatter; use crate::config::Config; use crate::timeparse::parse_time; use crate::regex::parse_regex; use crate::old::{entries_or_warning, time_or_warning, warn_if_needed}; use super::Command; // ---------------------------------------------------------------- // Things that are general to all commands that display in some way // ---------------------------------------------------------------- pub enum Sheet { All, Full, Sheet(String), } impl FromStr for Sheet { type Err = Error; fn from_str(name: &str) -> Result { Ok(match name { "all" => Sheet::All, "full" => Sheet::Full, name => Sheet::Sheet(name.into()), }) } } #[allow(clippy::too_many_arguments)] pub fn entries_for_display( start: Option>, end: Option>, sheet: Option, db: &mut D, out: &mut O, err: &mut E, format: Formatter, ids: bool, grep: Option, now: DateTime, ) -> Result<()> where D: Database, O: Write, E: Write, { let start = start.map(|s| time_or_warning(s, db)).transpose()?.map(|s| s.0); let end = end.map(|e| time_or_warning(e, db)).transpose()?.map(|e| e.0); let mut entries = match sheet { Some(Sheet::All) => db.entries_all_visible(start, end)?, Some(Sheet::Full) => db.entries_full(start, end)?, Some(Sheet::Sheet(name)) => db.entries_by_sheet(&name, start, end)?, None => { let current_sheet = db.current_sheet()?.unwrap_or_else(|| "default".into()); db.entries_by_sheet(¤t_sheet, start, end)? } }; if let Some(re) = grep { entries.retain(|e| re.is_match(&e.note.clone().unwrap_or_else(String::new))); } let (entries, needs_warning) = entries_or_warning(entries, db)?; format.print_formatted( entries, out, now, ids, )?; warn_if_needed(err, needs_warning)?; Ok(()) } // ------------------------------------ // The actual implementation of display // ------------------------------------ #[derive(Default)] pub struct Args { ids: bool, start: Option>, end: Option>, format: Formatter, grep: Option, sheet: Option, } impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { type Error = Error; fn try_from(matches: &'a ArgMatches) -> Result { Ok(Args { ids: matches.is_present("ids"), start: matches.value_of("start").map(|s| parse_time(s)).transpose()?, end: matches.value_of("end").map(|s| parse_time(s)).transpose()?, format: matches.value_of("format").unwrap().parse()?, grep: matches.value_of("grep").map(parse_regex).transpose()?, sheet: matches.value_of("sheet").map(|s| s.parse()).transpose()?, }) } } pub struct DisplayCommand { } impl<'a> Command<'a> for DisplayCommand { type Args = Args; fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config, now: DateTime) -> Result<()> where D: Database, O: Write, E: Write, { entries_for_display(args.start, args.end, args.sheet, db, out, err, args.format, args.ids, args.grep, now) } } #[cfg(test)] mod tests { use chrono::TimeZone; use ansi_term::Color::Yellow; use pretty_assertions::assert_eq; use crate::database::SqliteDatabase; use crate::test_utils::Ps; use super::*; #[test] fn display_as_local_time_if_previous_version() { std::env::set_var("TZ", "CST+6"); let args = Default::default(); let mut db = SqliteDatabase::from_path("assets/test_old_db.db").unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let config = Default::default(); DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default Day Start End Duration Notes Tue Jun 29, 2021 06:26:49 - 07:26:52 1:00:03 lets do some rust 1:00:03 ----------------------------------------------------------------------- Total 1:00:03 ")); assert_eq!( String::from_utf8_lossy(&err), format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), ); } #[test] fn filter_by_start() { let args = Args { format: Formatter::Csv, start: Some(Utc.ymd(2021, 6, 30).and_hms(10, 5, 0)), ..Default::default() }; let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let config = Default::default(); db.init().unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("start,end,note,sheet 2021-06-30T10:10:00.000000Z,,hola,default ")); assert_eq!( String::from_utf8_lossy(&err), String::new(), ); } #[test] fn filter_by_match() { let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); db.init().unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("adios".into()), "default".into()).unwrap(); entries_for_display(None, None, None, &mut db, &mut out, &mut err, Formatter::Csv, true, Some("io".parse().unwrap()), Utc::now()).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("id,start,end,note,sheet 2,2021-06-30T10:10:00.000000Z,,adios,default ")); assert_eq!( String::from_utf8_lossy(&err), String::new(), ); } #[test] fn entries_are_grouped_despite_database() { let args = Args { sheet: Some(Sheet::All), ..Default::default() }; let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let config = Default::default(); std::env::set_var("TZ", "CST+6"); db.init().unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "sheet2".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1".into()).unwrap(); DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: sheet1 Day Start End Duration Notes Wed Jun 30, 2021 04:00:00 - 05:00:00 1:00:00 06:00:00 - 07:00:00 1:00:00 2:00:00 ----------------------------------------------------------- Total 2:00:00 Timesheet: sheet2 Day Start End Duration Notes Wed Jun 30, 2021 05:00:00 - 06:00:00 1:00:00 1:00:00 ----------------------------------------------------------- Total 1:00:00 ----------------------------------------------------------- Grand total 3:00:00 ")); } #[test] fn entries_are_grouped_despite_database_full() { let args = Args { sheet: Some(Sheet::Full), ..Default::default() }; let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let config = Default::default(); std::env::set_var("TZ", "CST+6"); db.init().unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "_sheet2".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1".into()).unwrap(); DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("\ Timesheet: _sheet2 Day Start End Duration Notes Wed Jun 30, 2021 05:00:00 - 06:00:00 1:00:00 1:00:00 ----------------------------------------------------------- Total 1:00:00 Timesheet: sheet1 Day Start End Duration Notes Wed Jun 30, 2021 04:00:00 - 05:00:00 1:00:00 06:00:00 - 07:00:00 1:00:00 2:00:00 ----------------------------------------------------------- Total 2:00:00 ----------------------------------------------------------- Grand total 3:00:00 ")); } #[test] fn filter_old_database() { std::env::set_var("TZ", "CST+6"); let args = Args { format: Formatter::Csv, start: Some(Utc.ymd(2021, 6, 29).and_hms(12, 0, 0)), end: Some(Utc.ymd(2021, 6, 29).and_hms(13, 0, 0)), ..Default::default() }; let mut db = SqliteDatabase::from_path("assets/test_old_db.db").unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let config = Default::default(); // item in database: // start: 2021-06-29 06:26:49.580565 // end: 2021-06-29 07:26:52.816747 DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config, Utc::now()).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("start,end,note,sheet 2021-06-29T12:26:49.580565Z,2021-06-29T13:26:52.816747Z,lets do some rust,default ")); assert_eq!( String::from_utf8_lossy(&err), format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), ); } }