use std::convert::TryFrom; use std::io::Write; use clap::ArgMatches; use chrono::{DateTime, Utc}; use crate::database::{Database, DBVersion}; use crate::error::{Error, Result}; use crate::commands::Command; use crate::config::Config; use crate::timeparse::parse_time; use crate::old::{entries_or_warning, time_or_warning, warn_if_needed}; use crate::formatters::text; use crate::editor; #[derive(Default)] pub struct Args { id: Option, start: Option>, end: Option>, append: bool, r#move: Option, note: Option, } impl Args { /// returns true only if no argument was passed other that possibly --id. /// This means that an edit was requested without specifying what to edit, /// therefore let's edit the note because why not fn none_given(&self) -> bool { !(self.start.is_some() || self.end.is_some() || self.r#move.is_some() || self.note.is_some()) } } impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { type Error = Error; fn try_from(matches: &'a ArgMatches) -> Result { Ok(Args { id: matches.value_of("id").map(|i| i.parse().unwrap()), start: matches.value_of("start").map(|s| parse_time(s)).transpose()?, end: matches.value_of("end").map(|s| parse_time(s)).transpose()?, append: matches.is_present("append"), r#move: matches.value_of("move").map(|s| s.into()), note: matches.value_of("note").map(|s| s.into()), }) } } pub struct EditCommand {} impl<'a> Command<'a> for EditCommand { type Args = Args; fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> Result<()> where D: Database, O: Write, E: Write, { let current_sheet = db.current_sheet()?.unwrap_or_else(|| "default".to_owned()); let needs_warning = db.version()? == DBVersion::Timetrap; let entry = if let Some(id) = args.id { if let Some(entry) = db.entry_by_id(id)? { entry } else { writeln!(out, "Entry with id \"{}\" does not exist. Perhaps it was deleted", id)?; return Ok(()); } } else if let Some(entry) = db.last_entry_of_sheet(¤t_sheet)? { entry } else { writeln!(out, "No entries to edit in sheet \"{}\".", current_sheet)?; return Ok(()); }; // normalize the entry in case it comes from an old database let entry = entries_or_warning(vec![entry], db)?.0.into_iter().next().unwrap(); // try really hard to obtain the note let note = if let Some(new_note) = args.note { // either from the command's args if args.append { Some(entry.note.unwrap_or_else(|| "".to_owned()) + &config.append_notes_delimiter + &new_note) } else { Some(new_note) } } else if args.none_given() { // or from the editor if no arguments where given Some(editor::get_string(config.note_editor.as_deref(), entry.note)?) } else { // or just use watever was previously there is the user is editing // something else entry.note }; db.entry_update( entry.id, time_or_warning(args.start.unwrap_or(entry.start), db)?.0, args.end.or(entry.end).map(|e| time_or_warning(e, db)).transpose()?.map(|o| o.0), note, &args.r#move.unwrap_or(entry.sheet), )?; let updated_entry = entries_or_warning(vec![db.entry_by_id(entry.id)?.unwrap()], db)?.0; text::print_formatted( updated_entry, out, now, true, )?; warn_if_needed(err, needs_warning)?; Ok(()) } } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use chrono::{Duration, TimeZone}; use ansi_term::Color::Yellow; use crate::database::SqliteDatabase; use crate::test_utils::Ps; use super::*; #[test] fn edit_last_note() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let args = Args { note: Some("new note".into()), ..Default::default() }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); db.init().unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("should be left intact".into()), "sheet1").unwrap(); EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); let entry2 = db.entry_by_id(2).unwrap().unwrap(); assert_eq!(entry2.note, Some("should be left intact".into())); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 19:29:00 - 20:29:00 1:00:00 new note 1:00:00 -------------------------------------------------------------- Total 1:00:00 ")); assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); } #[test] fn edit_with_id() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let args = Args { id: Some(2), note: Some("new note".into()), ..Default::default() }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); db.init().unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("should be left intact".into()), "sheet1").unwrap(); EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: sheet1 ID Day Start End Duration Notes 2 Tue Aug 03, 2021 19:29:00 - 20:29:00 1:00:00 new note 1:00:00 -------------------------------------------------------------- Total 1:00:00 ")); assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); } #[test] fn edit_start() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let args = Args { start: Some(now - Duration::minutes(30)), ..Default::default() }; let an_hour_ago = now - Duration::hours(1); db.init().unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 19:59:00 - 20:29:00 0:30:00 a note 0:30:00 ------------------------------------------------------------ Total 0:30:00 ")); assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); } #[test] fn edit_end() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let args = Args { end: Some(now - Duration::minutes(30)), ..Default::default() }; let an_hour_ago = now - Duration::hours(1); db.init().unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 19:29:00 - 19:59:00 0:30:00 a note 0:30:00 ------------------------------------------------------------ Total 0:30:00 ")); assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); } #[test] fn edit_append() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let args = Args { note: Some("new note".into()), append: true, ..Default::default() }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); db.init().unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 19:29:00 - 20:29:00 1:00:00 a note new note 1:00:00 --------------------------------------------------------------------- Total 1:00:00 ")); assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); } #[test] fn edit_move() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let args = Args { r#move: Some("new sheet".to_owned()), ..Default::default() }; let an_hour_ago = now - Duration::hours(1); db.init().unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: new sheet ID Day Start End Duration Notes 1 Tue Aug 03, 2021 19:29:00 - 20:29:00 1:00:00 a note 1:00:00 ------------------------------------------------------------ Total 1:00:00 ")); assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); } #[test] fn non_default_delimiter() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let args = Args { note: Some("new note".into()), append: true, ..Default::default() }; let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let an_hour_ago = now - Duration::hours(1); let config = Config { append_notes_delimiter: ";".to_owned(), ..Default::default() }; db.init().unwrap(); db.entry_insert(an_hour_ago, Some(now), Some("a note".into()), "default").unwrap(); EditCommand::handle(args, &mut db, &mut out, &mut err, &config, now).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Aug 03, 2021 19:29:00 - 20:29:00 1:00:00 a note;new note 1:00:00 --------------------------------------------------------------------- Total 1:00:00 ")); assert_eq!(Ps(&String::from_utf8_lossy(&err)), Ps("")); } #[test] fn warn_old() { std::env::set_var("TZ", "UTC"); let mut db = SqliteDatabase::from_path("assets/test_old_db.db").unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0); let new_end = Utc.ymd(2021, 06, 29).and_hms(8, 26, 52); let args = Args { end: Some(new_end), ..Default::default() }; EditCommand::handle(args, &mut db, &mut out, &mut err, &Default::default(), now).unwrap(); assert_eq!(Ps(&String::from_utf8_lossy(&out)), Ps("Timesheet: default ID Day Start End Duration Notes 1 Tue Jun 29, 2021 06:26:49 - 08:26:52 2:00:02 lets do some rust 2:00:02 ----------------------------------------------------------------------- Total 2:00:02 ")); 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]")), ); } }