local timezone and timetrap database management
This commit is contained in:
parent
bd10e8582a
commit
af2a18c8db
|
@ -469,6 +469,7 @@ dependencies = [
|
|||
name = "tiempo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term 0.12.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dirs",
|
||||
|
|
|
@ -18,6 +18,7 @@ toml = "0.5"
|
|||
itertools = "0.10"
|
||||
textwrap = "0.14"
|
||||
terminal_size = "0.1"
|
||||
ansi_term = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.7.2"
|
||||
|
|
|
@ -3,16 +3,44 @@ use std::io::Write;
|
|||
use std::str::FromStr;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use chrono::{DateTime, Utc, Local, TimeZone};
|
||||
use chrono::{DateTime, Utc, Local, TimeZone, LocalResult};
|
||||
use terminal_size::{Width, terminal_size};
|
||||
use ansi_term::Color::Yellow;
|
||||
|
||||
use crate::error;
|
||||
use crate::database::Database;
|
||||
use crate::database::{Database, DBVersion};
|
||||
use crate::formatters::Formatter;
|
||||
use crate::config::Config;
|
||||
use crate::models::Entry;
|
||||
|
||||
use super::Command;
|
||||
|
||||
fn local_to_utc(t: DateTime<Utc>) -> error::Result<DateTime<Utc>> {
|
||||
let local_time = match Local.from_local_datetime(&t.naive_utc()) {
|
||||
LocalResult::None => return Err(error::Error::NoneLocalTime(t.naive_utc())),
|
||||
LocalResult::Single(t) => t,
|
||||
LocalResult::Ambiguous(t1, t2) => return Err(error::Error::AmbiguousLocalTime {
|
||||
orig: t.naive_utc(),
|
||||
t1, t2,
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(Utc.from_utc_datetime(&local_time.naive_utc()))
|
||||
}
|
||||
|
||||
fn local_to_utc_vec(entries: Vec<Entry>) -> error::Result<Vec<Entry>> {
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
Ok(Entry {
|
||||
start: local_to_utc(e.start)?,
|
||||
end: e.end.map(|t| local_to_utc(t)).transpose()?,
|
||||
..e
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn term_width() -> usize {
|
||||
if let Some((Width(w), _)) = terminal_size() {
|
||||
w as usize
|
||||
|
@ -64,8 +92,7 @@ impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct DisplayCommand {
|
||||
}
|
||||
pub struct DisplayCommand { }
|
||||
|
||||
impl<'a> Command<'a> for DisplayCommand {
|
||||
type Args = Args;
|
||||
|
@ -76,7 +103,7 @@ impl<'a> Command<'a> for DisplayCommand {
|
|||
O: Write,
|
||||
E: Write,
|
||||
{
|
||||
let sheets_to_display = match args.sheet {
|
||||
let entries = match args.sheet {
|
||||
Some(Sheet::All) => db.entries_all_visible()?,
|
||||
Some(Sheet::Full) => db.entries_full()?,
|
||||
Some(Sheet::Sheet(name)) => db.entries_by_sheet(&name)?,
|
||||
|
@ -87,11 +114,25 @@ impl<'a> Command<'a> for DisplayCommand {
|
|||
}
|
||||
};
|
||||
|
||||
let entries = if let DBVersion::Timetrap = db.version()? {
|
||||
// this indicates that times in the database are specified in the
|
||||
// local time and need to be converted to utc before displaying
|
||||
writeln!(
|
||||
err,
|
||||
"{} You are using the old timetrap format, it is advised that \
|
||||
you update your database using t migrate",
|
||||
Yellow.bold().paint("[WARNING]"),
|
||||
)?;
|
||||
|
||||
local_to_utc_vec(entries)?
|
||||
} else {
|
||||
entries
|
||||
};
|
||||
|
||||
args.format.print_formatted(
|
||||
sheets_to_display,
|
||||
entries,
|
||||
out,
|
||||
Utc::now(),
|
||||
Local.offset_from_utc_datetime(&Utc::now().naive_utc()),
|
||||
args.ids,
|
||||
term_width(),
|
||||
)
|
||||
|
@ -119,13 +160,13 @@ mod tests {
|
|||
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!(
|
||||
PrettyString(&String::from_utf8_lossy(&err)),
|
||||
PrettyString("[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate")
|
||||
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]")),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -134,4 +175,9 @@ mod tests {
|
|||
assert!(false, "start with a newly created database");
|
||||
assert!(false, "correctly display times in local timezone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_by_local_time_unless_timezone_specified() {
|
||||
assert!(false, "when using --start and --end times are assumed to be given in the local time unless they parse to a timezone aware time");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,11 @@ use chrono::{DateTime, Utc};
|
|||
use crate::error;
|
||||
use crate::models::{Entry, Meta};
|
||||
|
||||
pub enum DBVersion {
|
||||
Timetrap,
|
||||
Version(u16),
|
||||
}
|
||||
|
||||
pub trait Database {
|
||||
/// This is used to create tables and insert rows
|
||||
fn execute(&mut self, query: &str, params: &[&dyn ToSql]) -> error::Result<()>;
|
||||
|
@ -34,10 +39,25 @@ pub trait Database {
|
|||
|
||||
// Meta queries
|
||||
fn current_sheet(&self) -> error::Result<Option<String>> {
|
||||
let results = self.meta_query("select * from meta where key=?1", &[&"current_sheet"])?;
|
||||
let results = self.meta_query("select * from meta where key='current_sheet'", &[])?;
|
||||
|
||||
Ok(results.into_iter().next().map(|m| m.value))
|
||||
}
|
||||
|
||||
fn version(&self) -> error::Result<DBVersion> {
|
||||
let results = self.meta_query("select * from meta where key='database_version'", &[])?;
|
||||
|
||||
if let Some(v) = results.into_iter().next().map(|m| m.value) {
|
||||
Ok(DBVersion::Version(v.parse().map_err(|e| {
|
||||
error::Error::CorruptedData(format!(
|
||||
"Found value '{}' for key 'database_version' in meta table, which is not a valid integer",
|
||||
v
|
||||
))
|
||||
})?))
|
||||
} else {
|
||||
Ok(DBVersion::Timetrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SqliteDatabase {
|
||||
|
|
16
src/error.rs
16
src/error.rs
|
@ -2,6 +2,7 @@ use std::result;
|
|||
use std::path::PathBuf;
|
||||
|
||||
use thiserror::Error;
|
||||
use chrono::{NaiveDateTime, DateTime, Local};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
|
@ -30,7 +31,20 @@ pub enum Error {
|
|||
DateTimeParseError(#[from] chrono::format::ParseError),
|
||||
|
||||
#[error("IOError: {0}")]
|
||||
IOError(#[from] std::io::Error)
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("Corrupted data found in the database: {0}")]
|
||||
CorruptedData(String),
|
||||
|
||||
#[error("Trying to parse {0} as a time in your timezone led to no results")]
|
||||
NoneLocalTime(NaiveDateTime),
|
||||
|
||||
#[error("Trying to parse {orig} as a time in your timezone led to the ambiguous results {t1} and {t2}")]
|
||||
AmbiguousLocalTime {
|
||||
orig: NaiveDateTime,
|
||||
t1: DateTime<Local>,
|
||||
t2: DateTime<Local>,
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
|
|
|
@ -5,8 +5,8 @@ use std::borrow::Cow;
|
|||
use serde::{Serialize, Deserialize};
|
||||
use itertools::Itertools;
|
||||
use chrono::{
|
||||
DateTime, Utc, Offset, TimeZone, Duration, NaiveTime, Timelike,
|
||||
NaiveDateTime,
|
||||
DateTime, Utc, TimeZone, Duration, NaiveTime, Timelike, NaiveDateTime,
|
||||
Local,
|
||||
};
|
||||
|
||||
use crate::error;
|
||||
|
@ -70,9 +70,9 @@ impl Formatter {
|
|||
/// to the local timezone is given in `offset` to prevent this function from
|
||||
/// using a secondary effect to retrieve the time and conver dates. This
|
||||
/// also makes it easier to test.
|
||||
pub fn print_formatted<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool, term_width: usize) -> error::Result<()> {
|
||||
pub fn print_formatted<W: Write>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> error::Result<()> {
|
||||
match &self {
|
||||
Formatter::Text => self.print_formatted_text(entries, out, now, offset, ids, term_width)?,
|
||||
Formatter::Text => self.print_formatted_text(entries, out, now, ids, term_width)?,
|
||||
Formatter::Custom(name) => {
|
||||
}
|
||||
}
|
||||
|
@ -82,14 +82,9 @@ impl Formatter {
|
|||
|
||||
/// Print in the default text format. Assume entries are sorted by sheet and
|
||||
/// then by start
|
||||
fn print_formatted_text<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool, term_width: usize) -> error::Result<()> {
|
||||
fn print_formatted_text<W: Write>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, ids: bool, term_width: usize) -> error::Result<()> {
|
||||
let grouped_entries = entries.into_iter().group_by(|e| e.sheet.to_string());
|
||||
|
||||
// Build a timezone based on the offset given to this function. This
|
||||
// will later be used to properly group the entries by date in the local
|
||||
// timezone.
|
||||
let fixed_offset = offset.fix();
|
||||
|
||||
for (key, group) in grouped_entries.into_iter() {
|
||||
writeln!(out, "Timesheet: {}", key)?;
|
||||
|
||||
|
@ -97,18 +92,18 @@ impl Formatter {
|
|||
// A vector of lines to be printed, with all the components
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let entries_by_date = group.group_by(|e| fixed_offset.from_utc_datetime(&e.start.naive_utc()).date());
|
||||
let entries_by_date = group.group_by(|e| Local.from_utc_datetime(&e.start.naive_utc()).date());
|
||||
let mut total = Duration::seconds(0);
|
||||
|
||||
for (date, entries) in entries_by_date.into_iter() {
|
||||
let mut daily = Duration::seconds(0);
|
||||
|
||||
for (i, entry) in entries.into_iter().enumerate() {
|
||||
let start = format_start(fixed_offset.from_utc_datetime(&entry.start.naive_utc()).time());
|
||||
let start = format_start(Local.from_utc_datetime(&entry.start.naive_utc()).time());
|
||||
let end = entry.end.map(|t| {
|
||||
format_end(
|
||||
fixed_offset.from_utc_datetime(&entry.start.naive_utc()).naive_local(),
|
||||
fixed_offset.from_utc_datetime(&t.naive_utc()).naive_local()
|
||||
Local.from_utc_datetime(&entry.start.naive_utc()).naive_local(),
|
||||
Local.from_utc_datetime(&t.naive_utc()).naive_local()
|
||||
)
|
||||
}).unwrap_or(" ".into());
|
||||
let duration = entry.end.unwrap_or(now) - entry.start;
|
||||
|
@ -231,6 +226,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_constrained_lines_long_text() {
|
||||
std::env::set_var("TZ", "UTC");
|
||||
assert_eq!(constrained_lines(LONG_NOTE, 46), vec![
|
||||
"chatting with bob about upcoming task,",
|
||||
"district sharing of images, how the user",
|
||||
|
@ -245,6 +241,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_text_output() {
|
||||
std::env::set_var("TZ", "UTC");
|
||||
let formatter = Formatter::Text;
|
||||
let mut output = Vec::new();
|
||||
let entries = vec![
|
||||
|
@ -255,9 +252,8 @@ mod tests {
|
|||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let offset = Utc;
|
||||
|
||||
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
|
||||
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
|
||||
|
||||
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
|
||||
Day Start End Duration Notes
|
||||
|
@ -274,6 +270,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_text_output_with_millis() {
|
||||
std::env::set_var("TZ", "UTC");
|
||||
let formatter = Formatter::Text;
|
||||
let mut output = Vec::new();
|
||||
let entries = vec![
|
||||
|
@ -281,9 +278,8 @@ mod tests {
|
|||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let offset = Utc;
|
||||
|
||||
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
|
||||
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
|
||||
|
||||
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
|
||||
Day Start End Duration Notes
|
||||
|
@ -306,7 +302,7 @@ mod tests {
|
|||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let offset = Utc;
|
||||
|
||||
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
|
||||
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
|
||||
|
||||
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
|
||||
Day Start End Duration Notes
|
||||
|
@ -333,7 +329,7 @@ mod tests {
|
|||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let offset = Utc;
|
||||
|
||||
formatter.print_formatted(entries, &mut output, now, offset, true, 100).unwrap();
|
||||
formatter.print_formatted(entries, &mut output, now, true, 100).unwrap();
|
||||
|
||||
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
|
||||
ID Day Start End Duration Notes
|
||||
|
@ -365,7 +361,7 @@ mod tests {
|
|||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let offset = Utc;
|
||||
|
||||
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
|
||||
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
|
||||
|
||||
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
|
||||
Day Start End Duration Notes
|
||||
|
@ -400,7 +396,7 @@ mod tests {
|
|||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let offset = Utc;
|
||||
|
||||
formatter.print_formatted(entries, &mut output, now, offset, true, 100).unwrap();
|
||||
formatter.print_formatted(entries, &mut output, now, true, 100).unwrap();
|
||||
|
||||
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
|
||||
ID Day Start End Duration Notes
|
||||
|
@ -435,7 +431,7 @@ mod tests {
|
|||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let offset = Utc;
|
||||
|
||||
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
|
||||
formatter.print_formatted(entries, &mut output, now, false, 100).unwrap();
|
||||
|
||||
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
|
||||
Day Start End Duration Notes
|
||||
|
|
Loading…
Reference in New Issue