From 092765f7693b1e1ca657afc79f9e9c65b5193045 Mon Sep 17 00:00:00 2001 From: daladim Date: Wed, 14 Apr 2021 23:09:41 +0200 Subject: [PATCH] Tasks include a COMPLETED timestamp --- src/ical/builder.rs | 15 +++++++++++---- src/ical/parser.rs | 34 ++++++++++++++++++++++++---------- src/task.rs | 28 ++++++++++++++++------------ 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/ical/builder.rs b/src/ical/builder.rs index 9671aed..d511382 100644 --- a/src/ical/builder.rs +++ b/src/ical/builder.rs @@ -3,7 +3,7 @@ use std::error::Error; use chrono::{DateTime, Utc}; -use ics::properties::{LastModified, Status, Summary}; +use ics::properties::{Completed, LastModified, Status, Summary}; use ics::{ICalendar, ToDo}; use crate::item::Item; @@ -26,6 +26,10 @@ pub fn build_from(item: &Item) -> Result> { match item { Item::Task(t) => { + t.completion_date().map(|dt| todo.push( + Completed::new(format_date_time(dt)) + )); + let status = if t.completed() { Status::completed() } else { Status::needs_action() }; todo.push(status); }, @@ -53,11 +57,13 @@ mod tests { #[test] fn test_ical_from_task() { let cal_id = "http://my.calend.ar/id".parse().unwrap(); - let now = format_date_time(&Utc::now()); + let now = Utc::now(); + let s_now = format_date_time(&now); - let task = Item::Task(Task::new( + let mut task = Item::Task(Task::new( String::from("This is a task with ÜTF-8 characters"), true, &cal_id )); + task.unwrap_task_mut().set_completed_on(Some(now)); let expected_ical = format!("BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ PRODID:-//{}//{}//EN\r\n\ @@ -65,9 +71,10 @@ mod tests { UID:{}\r\n\ DTSTAMP:{}\r\n\ SUMMARY:This is a task with ÜTF-8 characters\r\n\ + COMPLETED:{}\r\n\ STATUS:COMPLETED\r\n\ END:VTODO\r\n\ - END:VCALENDAR\r\n", ORG_NAME, PRODUCT_NAME, task.uid(), now); + END:VCALENDAR\r\n", ORG_NAME, PRODUCT_NAME, task.uid(), s_now, s_now); let ical = build_from(&task); assert_eq!(ical.unwrap(), expected_ical); diff --git a/src/ical/parser.rs b/src/ical/parser.rs index 92eebad..8fb218a 100644 --- a/src/ical/parser.rs +++ b/src/ical/parser.rs @@ -33,6 +33,7 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result< let mut uid = None; let mut completed = false; let mut last_modified = None; + let mut completion_date = None; for prop in &todo.properties { if prop.name == "SUMMARY" { name = prop.value.clone(); @@ -53,15 +54,12 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result< if prop.name == "DTSTAMP" { // this property specifies the date and time that the information associated with // the calendar component was last revised in the calendar store. - last_modified = prop.value.as_ref() - .and_then(|s| { - parse_date_time(s) - .map_err(|err| { - log::warn!("Invalid DTSTAMP: {}", s); - err - }) - .ok() - }) + last_modified = parse_date_time_from_property(&prop.value) + } + if prop.name == "COMPLETED" { + // this property specifies the date and time that the information associated with + // the calendar component was last revised in the calendar store. + completion_date = parse_date_time_from_property(&prop.value) } } let name = match name { @@ -76,8 +74,13 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result< Some(dt) => dt, None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_id).into()), }; + if completion_date.is_none() && completed || + completion_date.is_some() && !completed + { + log::warn!("Inconsistant iCal data: completion date is {:?} but completion status is {:?}", completion_date, completed); + } - Item::Task(Task::new_with_parameters(name, completed, uid, item_id, sync_status, last_modified)) + Item::Task(Task::new_with_parameters(name, uid, item_id, sync_status, last_modified, completion_date)) }, }; @@ -94,6 +97,17 @@ fn parse_date_time(dt: &str) -> Result, chrono::format::ParseError Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S") } +fn parse_date_time_from_property(value: &Option) -> Option> { + value.as_ref() + .and_then(|s| { + parse_date_time(s) + .map_err(|err| { + log::warn!("Invalid timestamp: {}", s); + err + }) + .ok() + }) +} enum CurrentType<'a> { diff --git a/src/task.rs b/src/task.rs index 8dac21e..827c90a 100644 --- a/src/task.rs +++ b/src/task.rs @@ -23,8 +23,8 @@ pub struct Task { /// The display name of the task name: String, - /// The completion of the task - completed: bool, + /// The time it was completed. Set to None if this task has not been completed + completion_date: Option>, } impl Task { @@ -35,17 +35,19 @@ impl Task { let new_sync_status = SyncStatus::NotSynced; let new_uid = Uuid::new_v4().to_hyphenated().to_string(); let new_last_modified = Utc::now(); - Self::new_with_parameters(name, completed, new_uid, new_item_id, new_sync_status, new_last_modified) + let new_completion_date = if completed { Some(Utc::now()) } else { None }; + Self::new_with_parameters(name, new_uid, new_item_id, new_sync_status, new_last_modified, new_completion_date) } /// Create a new Task instance, that may be synced already - pub fn new_with_parameters(name: String, completed: bool, uid: String, id: ItemId, sync_status: SyncStatus, last_modified: DateTime) -> Self { + pub fn new_with_parameters(name: String, uid: String, id: ItemId, + sync_status: SyncStatus, last_modified: DateTime, completion_date: Option>) -> Self { Self { id, uid, name, sync_status, - completed, + completion_date, last_modified, } } @@ -53,14 +55,16 @@ impl Task { pub fn id(&self) -> &ItemId { &self.id } pub fn uid(&self) -> &str { &self.uid } pub fn name(&self) -> &str { &self.name } - pub fn completed(&self) -> bool { self.completed } + pub fn completed(&self) -> bool { self.completion_date.is_some() } pub fn sync_status(&self) -> &SyncStatus { &self.sync_status } pub fn last_modified(&self) -> &DateTime { &self.last_modified } + pub fn completion_date(&self) -> Option<&DateTime> { self.completion_date.as_ref() } pub fn has_same_observable_content_as(&self, other: &Task) -> bool { self.id == other.id && self.name == other.name - && self.completed == other.completed + && self.completion_date == other.completion_date + && self.last_modified == other.last_modified // sync status must be the same variant, but we ignore its embedded version tag && std::mem::discriminant(&self.sync_status) == std::mem::discriminant(&other.sync_status) } @@ -102,16 +106,16 @@ impl Task { self.name = new_name; } - /// Set the completion status - pub fn set_completed(&mut self, new_value: bool) { + /// Set the completion date (or pass None to un-complete the task) + pub fn set_completed_on(&mut self, new_completion_date: Option>) { self.update_sync_status(); self.update_last_modified(); - self.completed = new_value; + self.completion_date = new_completion_date; } #[cfg(feature = "local_calendar_mocks_remote_calendars")] /// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters" - pub fn mock_remote_calendar_set_completed(&mut self, new_value: bool) { + pub fn mock_remote_calendar_set_completed_on(&mut self, nnew_completion_date: Option>) { self.sync_status = SyncStatus::random_synced(); - self.completed = new_value; + self.completion_date = new_completion_date; } }