Tasks include a COMPLETED timestamp
This commit is contained in:
parent
8e35f4c579
commit
092765f769
3 changed files with 51 additions and 26 deletions
|
@ -3,7 +3,7 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use ics::properties::{LastModified, Status, Summary};
|
use ics::properties::{Completed, LastModified, Status, Summary};
|
||||||
use ics::{ICalendar, ToDo};
|
use ics::{ICalendar, ToDo};
|
||||||
|
|
||||||
use crate::item::Item;
|
use crate::item::Item;
|
||||||
|
@ -26,6 +26,10 @@ pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
Item::Task(t) => {
|
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() };
|
let status = if t.completed() { Status::completed() } else { Status::needs_action() };
|
||||||
todo.push(status);
|
todo.push(status);
|
||||||
},
|
},
|
||||||
|
@ -53,11 +57,13 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ical_from_task() {
|
fn test_ical_from_task() {
|
||||||
let cal_id = "http://my.calend.ar/id".parse().unwrap();
|
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
|
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\
|
let expected_ical = format!("BEGIN:VCALENDAR\r\n\
|
||||||
VERSION:2.0\r\n\
|
VERSION:2.0\r\n\
|
||||||
PRODID:-//{}//{}//EN\r\n\
|
PRODID:-//{}//{}//EN\r\n\
|
||||||
|
@ -65,9 +71,10 @@ mod tests {
|
||||||
UID:{}\r\n\
|
UID:{}\r\n\
|
||||||
DTSTAMP:{}\r\n\
|
DTSTAMP:{}\r\n\
|
||||||
SUMMARY:This is a task with ÜTF-8 characters\r\n\
|
SUMMARY:This is a task with ÜTF-8 characters\r\n\
|
||||||
|
COMPLETED:{}\r\n\
|
||||||
STATUS:COMPLETED\r\n\
|
STATUS:COMPLETED\r\n\
|
||||||
END:VTODO\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);
|
let ical = build_from(&task);
|
||||||
assert_eq!(ical.unwrap(), expected_ical);
|
assert_eq!(ical.unwrap(), expected_ical);
|
||||||
|
|
|
@ -33,6 +33,7 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||||
let mut uid = None;
|
let mut uid = None;
|
||||||
let mut completed = false;
|
let mut completed = false;
|
||||||
let mut last_modified = None;
|
let mut last_modified = None;
|
||||||
|
let mut completion_date = None;
|
||||||
for prop in &todo.properties {
|
for prop in &todo.properties {
|
||||||
if prop.name == "SUMMARY" {
|
if prop.name == "SUMMARY" {
|
||||||
name = prop.value.clone();
|
name = prop.value.clone();
|
||||||
|
@ -53,15 +54,12 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||||
if prop.name == "DTSTAMP" {
|
if prop.name == "DTSTAMP" {
|
||||||
// this property specifies the date and time that the information associated with
|
// this property specifies the date and time that the information associated with
|
||||||
// the calendar component was last revised in the calendar store.
|
// the calendar component was last revised in the calendar store.
|
||||||
last_modified = prop.value.as_ref()
|
last_modified = parse_date_time_from_property(&prop.value)
|
||||||
.and_then(|s| {
|
}
|
||||||
parse_date_time(s)
|
if prop.name == "COMPLETED" {
|
||||||
.map_err(|err| {
|
// this property specifies the date and time that the information associated with
|
||||||
log::warn!("Invalid DTSTAMP: {}", s);
|
// the calendar component was last revised in the calendar store.
|
||||||
err
|
completion_date = parse_date_time_from_property(&prop.value)
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let name = match name {
|
let name = match name {
|
||||||
|
@ -76,8 +74,13 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||||
Some(dt) => dt,
|
Some(dt) => dt,
|
||||||
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_id).into()),
|
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<DateTime<Utc>, chrono::format::ParseError
|
||||||
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S")
|
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> {
|
||||||
|
value.as_ref()
|
||||||
|
.and_then(|s| {
|
||||||
|
parse_date_time(s)
|
||||||
|
.map_err(|err| {
|
||||||
|
log::warn!("Invalid timestamp: {}", s);
|
||||||
|
err
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
enum CurrentType<'a> {
|
enum CurrentType<'a> {
|
||||||
|
|
28
src/task.rs
28
src/task.rs
|
@ -23,8 +23,8 @@ pub struct Task {
|
||||||
|
|
||||||
/// The display name of the task
|
/// The display name of the task
|
||||||
name: String,
|
name: String,
|
||||||
/// The completion of the task
|
/// The time it was completed. Set to None if this task has not been completed
|
||||||
completed: bool,
|
completion_date: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
|
@ -35,17 +35,19 @@ impl Task {
|
||||||
let new_sync_status = SyncStatus::NotSynced;
|
let new_sync_status = SyncStatus::NotSynced;
|
||||||
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
|
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
|
||||||
let new_last_modified = Utc::now();
|
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
|
/// 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<Utc>) -> Self {
|
pub fn new_with_parameters(name: String, uid: String, id: ItemId,
|
||||||
|
sync_status: SyncStatus, last_modified: DateTime<Utc>, completion_date: Option<DateTime<Utc>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
uid,
|
uid,
|
||||||
name,
|
name,
|
||||||
sync_status,
|
sync_status,
|
||||||
completed,
|
completion_date,
|
||||||
last_modified,
|
last_modified,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,14 +55,16 @@ impl Task {
|
||||||
pub fn id(&self) -> &ItemId { &self.id }
|
pub fn id(&self) -> &ItemId { &self.id }
|
||||||
pub fn uid(&self) -> &str { &self.uid }
|
pub fn uid(&self) -> &str { &self.uid }
|
||||||
pub fn name(&self) -> &str { &self.name }
|
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 sync_status(&self) -> &SyncStatus { &self.sync_status }
|
||||||
pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified }
|
pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified }
|
||||||
|
pub fn completion_date(&self) -> Option<&DateTime<Utc>> { self.completion_date.as_ref() }
|
||||||
|
|
||||||
pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
|
pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
|
||||||
self.id == other.id
|
self.id == other.id
|
||||||
&& self.name == other.name
|
&& 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
|
// 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)
|
&& std::mem::discriminant(&self.sync_status) == std::mem::discriminant(&other.sync_status)
|
||||||
}
|
}
|
||||||
|
@ -102,16 +106,16 @@ impl Task {
|
||||||
self.name = new_name;
|
self.name = new_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the completion status
|
/// Set the completion date (or pass None to un-complete the task)
|
||||||
pub fn set_completed(&mut self, new_value: bool) {
|
pub fn set_completed_on(&mut self, new_completion_date: Option<DateTime<Utc>>) {
|
||||||
self.update_sync_status();
|
self.update_sync_status();
|
||||||
self.update_last_modified();
|
self.update_last_modified();
|
||||||
self.completed = new_value;
|
self.completion_date = new_completion_date;
|
||||||
}
|
}
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
/// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters"
|
/// 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<DateTime<Utc>>) {
|
||||||
self.sync_status = SyncStatus::random_synced();
|
self.sync_status = SyncStatus::random_synced();
|
||||||
self.completed = new_value;
|
self.completion_date = new_completion_date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue