diff --git a/.gitignore b/.gitignore index ea8c4bf..86d5801 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +fake_old_config.yml +.env diff --git a/src/commands/in.rs b/src/commands/in.rs index 2934c82..e96e8d4 100644 --- a/src/commands/in.rs +++ b/src/commands/in.rs @@ -10,7 +10,9 @@ use crate::editor; use crate::commands::Command; use crate::config::Config; use crate::timeparse::parse_time; +use crate::old::{start_or_warning, warn_if_needed}; +#[derive(Default)] pub struct Args { at: Option>, note: Option, @@ -32,7 +34,7 @@ pub struct InCommand {} impl<'a> Command<'a> for InCommand { type Args = Args; - fn handle(args: Args, db: &mut D, _out: &mut O, _err: &mut E, config: &Config, now: DateTime) -> error::Result<()> + fn handle(args: Args, db: &mut D, out: &mut O, err: &mut E, config: &Config, now: DateTime) -> error::Result<()> where D: Database, O: Write, @@ -40,13 +42,30 @@ impl<'a> Command<'a> for InCommand { { let start = args.at.unwrap_or(now); let sheet = db.current_sheet()?.unwrap_or("default".into()); + + if db.has_running_entry(&sheet)? { + writeln!(out, "Timer is already running for sheet '{}'", sheet)?; + + return Ok(()); + } + let note = if let Some(note) = args.note { - note + Some(note) } else { - editor::get_string(config)? + if !config.require_note { + None + } else { + Some(editor::get_string(config)?) + } }; - db.entry_insert(start, None, Some(note), sheet)?; + let (start, needs_warning) = start_or_warning(start, db)?; + + db.entry_insert(start, None, note.map(|n| n.trim().to_owned()), &sheet)?; + + writeln!(out, "Checked into sheet \"{}\".", sheet)?; + + warn_if_needed(err, needs_warning)?; Ok(()) } @@ -54,13 +73,17 @@ impl<'a> Command<'a> for InCommand { #[cfg(test)] mod tests { - use super::*; + use pretty_assertions::assert_eq; + use ansi_term::Color::Yellow; + use chrono::TimeZone; + use crate::test_utils::PrettyString; use crate::database::SqliteDatabase; + use super::*; + #[test] - #[ignore] - fn test_handles_new_entry() { + fn handles_new_entry() { let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { at: None, @@ -68,16 +91,26 @@ mod tests { }; let mut out = Vec::new(); let mut err = Vec::new(); + let now = Utc::now(); - assert!(false, "there are no entries"); + d.init().unwrap(); - InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), Utc::now()).unwrap(); + assert_eq!(d.entries_full(None, None).unwrap().len(), 0); - assert!(false, "there is one entry"); + InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); + + let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + + assert_eq!(e.note, Some("hola".into())); + assert_eq!(e.start, now); + assert_eq!(e.end, None); + assert_eq!(e.sheet, "default".to_owned()); + + assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Checked into sheet \"default\".\n")); + assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString("")); } #[test] - #[ignore] fn test_handles_already_running_entry() { let mut d = SqliteDatabase::from_memory().unwrap(); let args = Args { @@ -86,24 +119,106 @@ mod tests { }; let mut out = Vec::new(); let mut err = Vec::new(); + let now = Utc::now(); - assert!(false, "there are no entries"); + d.init().unwrap(); + + d.entry_insert(now, None, None, "default".into()).unwrap(); + + assert_eq!(d.entries_full(None, None).unwrap().len(), 1); InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), Utc::now()).unwrap(); - assert!(false, "there are still no entries"); - assert!(false, "a message is issued to the user warning a running entry in this sheet"); + assert_eq!(d.entries_full(None, None).unwrap().len(), 1); + + assert_eq!( + PrettyString(&String::from_utf8_lossy(&out)), + PrettyString("Timer is already running for sheet 'default'\n") + ); } #[test] - #[ignore] - fn test_with_no_time_given_uses_now() { - assert!(false); + fn no_note_and_no_mandatory_leaves_none() { + let mut d = SqliteDatabase::from_memory().unwrap(); + let args = Default::default(); + let mut out = Vec::new(); + let mut err = Vec::new(); + let now = Utc::now(); + let config = Config { + require_note: false, + ..Default::default() + }; + + d.init().unwrap(); + + assert_eq!(d.entries_full(None, None).unwrap().len(), 0); + + InCommand::handle(args, &mut d, &mut out, &mut err, &config, now).unwrap(); + + let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + + assert_eq!(e.note, None); + assert_eq!(e.start, now); + assert_eq!(e.end, None); + assert_eq!(e.sheet, "default".to_owned()); } #[test] - #[ignore] - fn test_sets_given_time() { - assert!(false); + fn warns_if_old_database() { + let mut d = SqliteDatabase::from_memory().unwrap(); + let args = Args { + at: None, + note: Some("hola".into()), + }; + let mut out = Vec::new(); + let mut err = Vec::new(); + let now = Utc::now(); + + d.init_old().unwrap(); + + assert_eq!(d.entries_full(None, None).unwrap().len(), 0); + + InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); + + let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + + assert_eq!(e.note, Some("hola".into())); + assert_eq!(e.start, Utc.from_utc_datetime(&now.naive_local())); + assert_eq!(e.end, None); + assert_eq!(e.sheet, "default".to_owned()); + + assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Checked into sheet \"default\".\n")); + assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString(&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 notes_are_trimmed() { + let mut d = SqliteDatabase::from_memory().unwrap(); + let args = Args { + at: None, + note: Some("hola\n".into()), + }; + let mut out = Vec::new(); + let mut err = Vec::new(); + let now = Utc::now(); + + d.init().unwrap(); + + assert_eq!(d.entries_full(None, None).unwrap().len(), 0); + + InCommand::handle(args, &mut d, &mut out, &mut err, &Default::default(), now).unwrap(); + + let e = d.entries_full(None, None).unwrap().into_iter().next().unwrap(); + + assert_eq!(e.note, Some("hola".into())); + assert_eq!(e.start, now); + assert_eq!(e.end, None); + assert_eq!(e.sheet, "default".to_owned()); + + assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Checked into sheet \"default\".\n")); + assert_eq!(PrettyString(&String::from_utf8_lossy(&err)), PrettyString("")); } } diff --git a/src/database.rs b/src/database.rs index 4da8aca..6e4775e 100644 --- a/src/database.rs +++ b/src/database.rs @@ -22,7 +22,18 @@ pub trait Database { // ---------- // Migrations // ---------- + /// Create a database in the new database format. Actually the same format + /// just it has an entry in the meta table that indicates the database + /// version. fn init(&mut self) -> Result<()> { + self.init_old()?; + self.execute("INSERT INTO meta (key, value) VALUES ('database_version', 1)", &[])?; + + Ok(()) + } + + /// Creates the tables for the old database format + fn init_old(&mut self) -> Result<()> { self.execute("CREATE TABLE `entries` ( `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,\ @@ -39,7 +50,6 @@ pub trait Database { `value` varchar(255) ) ", &[])?; - self.execute("INSERT INTO meta (key, value) VALUES ('database_version', 1)", &[])?; Ok(()) } @@ -134,12 +144,16 @@ pub trait Database { } } - fn entry_insert(&mut self, start: DateTime, end: Option>, note: Option, sheet: String) -> Result<()> { + fn entry_insert(&mut self, start: DateTime, end: Option>, note: Option, sheet: &str) -> Result<()> { self.execute("insert into entries (start, end, note, sheet) values (?1, ?2, ?3, ?4)", &[ &start, &end, ¬e, &sheet, ]) } + fn has_running_entry(&self, sheet: &str) -> Result { + Ok(self.entry_query("select * from entries where end is null and sheet=?1", &[&sheet])?.len() != 0) + } + // Meta queries fn current_sheet(&self) -> Result> { let results = self.meta_query("select * from meta where key='current_sheet'", &[])?; diff --git a/src/old.rs b/src/old.rs index b6c5ac6..6d02086 100644 --- a/src/old.rs +++ b/src/old.rs @@ -7,6 +7,10 @@ use crate::error::{Error, Result}; use crate::models::Entry; use crate::database::{Database, DBVersion}; +/// Treat t as if it wasnt actually in Utc but in the local timezone and return +/// the actual Utc time. +/// +/// Used to convert times from the old database version. fn local_to_utc(t: DateTime) -> Result> { let local_time = match Local.from_local_datetime(&t.naive_utc()) { LocalResult::None => return Err(Error::NoneLocalTime(t.naive_utc().to_string())), @@ -21,6 +25,16 @@ fn local_to_utc(t: DateTime) -> Result> { Ok(Utc.from_utc_datetime(&local_time.naive_utc())) } +/// takes an otherwise perfectly good timestamp in Utc and turns it into the +/// local timezone, but using the same DateTime type. +/// +/// Used to insert times into the old database format. +fn utc_to_local(t: DateTime) -> DateTime { + Utc.from_utc_datetime(&t.naive_local()) +} + +/// Maps an entire vector of entries from the old database format to the new, +/// converting their timestamps from the local timezone to Utc. fn local_to_utc_vec(entries: Vec) -> Result> { entries .into_iter() @@ -34,6 +48,8 @@ fn local_to_utc_vec(entries: Vec) -> Result> { .collect() } +/// the logic used by many subcommands that transform entries from the old +/// format to the new. Used in conjunction with warn_if_needed. pub fn entries_or_warning(entries: Vec, db: &D) -> Result<(Vec, bool)> { if let DBVersion::Timetrap = db.version()? { // this indicates that times in the database are specified in the @@ -45,6 +61,15 @@ pub fn entries_or_warning(entries: Vec, db: &D) -> Result<(V } } +pub fn start_or_warning(time: DateTime, db: &D) -> Result<(DateTime, bool)> { + if let DBVersion::Timetrap = db.version()? { + Ok((utc_to_local(time), true)) + } else { + Ok((time, false)) + } +} + +/// emits the appropiate warning if the old database format was detected. pub fn warn_if_needed(err: &mut E, needs_warning: bool) -> Result<()> { if needs_warning { writeln!(