Items include a last modified (DTSTAMP) date
This commit is contained in:
parent
e24fab2ccb
commit
8e35f4c579
7 changed files with 119 additions and 12 deletions
44
Cargo.lock
generated
44
Cargo.lock
generated
|
@ -73,6 +73,20 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"time",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
@ -437,6 +451,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"ical",
|
"ical",
|
||||||
"ics",
|
"ics",
|
||||||
|
@ -478,6 +493,25 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
|
@ -886,6 +920,16 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
|
@ -25,3 +25,4 @@ uuid = { version = "0.8", features = ["v4"] }
|
||||||
sanitize-filename = "0.3"
|
sanitize-filename = "0.3"
|
||||||
ical = "0.7"
|
ical = "0.7"
|
||||||
ics = "0.5"
|
ics = "0.5"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//! Calendar events
|
//! Calendar events
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use crate::item::ItemId;
|
use crate::item::ItemId;
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
|
@ -31,6 +32,10 @@ impl Event {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_modified(&self) -> &DateTime<Utc> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sync_status(&self) -> &SyncStatus {
|
pub fn sync_status(&self) -> &SyncStatus {
|
||||||
&self.sync_status
|
&self.sync_status
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use ics::properties::{Comment, Status, Summary};
|
use chrono::{DateTime, Utc};
|
||||||
|
use ics::properties::{LastModified, Status, Summary};
|
||||||
use ics::{ICalendar, ToDo};
|
use ics::{ICalendar, ToDo};
|
||||||
|
|
||||||
use crate::item::Item;
|
use crate::item::Item;
|
||||||
|
@ -14,9 +15,14 @@ fn ical_product_id() -> String {
|
||||||
|
|
||||||
/// Create an iCal item from a `crate::item::Item`
|
/// Create an iCal item from a `crate::item::Item`
|
||||||
pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
|
pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
|
||||||
let mut todo = ToDo::new(item.uid(), "20181021T190000");
|
let s_last_modified = format_date_time(item.last_modified());
|
||||||
todo.push(Summary::new("Take pictures of squirrels (with ÜTF-8 chars)"));
|
|
||||||
todo.push(Comment::new("That's really something I'd like to do one day"));
|
let mut todo = ToDo::new(
|
||||||
|
item.uid(),
|
||||||
|
s_last_modified.clone(),
|
||||||
|
);
|
||||||
|
todo.push(LastModified::new(s_last_modified));
|
||||||
|
todo.push(Summary::new(item.name()));
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
Item::Task(t) => {
|
Item::Task(t) => {
|
||||||
|
@ -34,6 +40,9 @@ pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
|
||||||
Ok(calendar.to_string())
|
Ok(calendar.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_date_time(dt: &DateTime<Utc>) -> String {
|
||||||
|
dt.format("%Y%m%dT%H%M%S").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -44,20 +53,21 @@ 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 task = Item::Task(Task::new(
|
let task = Item::Task(Task::new(
|
||||||
String::from("This is a task"), true, &cal_id
|
String::from("This is a task with ÜTF-8 characters"), true, &cal_id
|
||||||
));
|
));
|
||||||
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\
|
||||||
BEGIN:VTODO\r\n\
|
BEGIN:VTODO\r\n\
|
||||||
UID:{}\r\n\
|
UID:{}\r\n\
|
||||||
DTSTAMP:20181021T190000\r\n\
|
DTSTAMP:{}\r\n\
|
||||||
SUMMARY:Take pictures of squirrels (with ÜTF-8 chars)\r\n\
|
SUMMARY:This is a task with ÜTF-8 characters\r\n\
|
||||||
COMMENT:That's really something I'd like to do one day\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());
|
END:VCALENDAR\r\n", ORG_NAME, PRODUCT_NAME, task.uid(), now);
|
||||||
|
|
||||||
let ical = build_from(&task);
|
let ical = build_from(&task);
|
||||||
assert_eq!(ical.unwrap(), expected_ical);
|
assert_eq!(ical.unwrap(), expected_ical);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
|
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
|
||||||
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
|
||||||
use crate::Item;
|
use crate::Item;
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
|
@ -31,6 +32,7 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||||
let mut name = None;
|
let mut name = None;
|
||||||
let mut uid = None;
|
let mut uid = None;
|
||||||
let mut completed = false;
|
let mut completed = false;
|
||||||
|
let mut last_modified = 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();
|
||||||
|
@ -48,6 +50,19 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||||
if prop.name == "UID" {
|
if prop.name == "UID" {
|
||||||
uid = prop.value.clone();
|
uid = prop.value.clone();
|
||||||
}
|
}
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let name = match name {
|
let name = match name {
|
||||||
Some(name) => name,
|
Some(name) => name,
|
||||||
|
@ -57,8 +72,12 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||||
Some(uid) => uid,
|
Some(uid) => uid,
|
||||||
None => return Err(format!("Missing UID for item {}", item_id).into()),
|
None => return Err(format!("Missing UID for item {}", item_id).into()),
|
||||||
};
|
};
|
||||||
|
let last_modified = match last_modified {
|
||||||
|
Some(dt) => dt,
|
||||||
|
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_id).into()),
|
||||||
|
};
|
||||||
|
|
||||||
Item::Task(Task::new_with_parameters(name, completed, uid, item_id, sync_status))
|
Item::Task(Task::new_with_parameters(name, completed, uid, item_id, sync_status, last_modified))
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,6 +90,12 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
|
||||||
Ok(item)
|
Ok(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
|
||||||
|
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
enum CurrentType<'a> {
|
enum CurrentType<'a> {
|
||||||
Event(&'a IcalEvent),
|
Event(&'a IcalEvent),
|
||||||
Todo(&'a IcalTodo),
|
Todo(&'a IcalTodo),
|
||||||
|
@ -171,6 +196,7 @@ END:VCALENDAR
|
||||||
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
|
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
|
||||||
assert_eq!(task.completed(), false);
|
assert_eq!(task.completed(), false);
|
||||||
assert_eq!(task.sync_status(), &sync_status);
|
assert_eq!(task.sync_status(), &sync_status);
|
||||||
|
assert_eq!(task.last_modified(), &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -5,6 +5,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use crate::resource::Resource;
|
use crate::resource::Resource;
|
||||||
use crate::calendar::CalendarId;
|
use crate::calendar::CalendarId;
|
||||||
|
@ -39,6 +40,13 @@ impl Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn last_modified(&self) -> &DateTime<Utc> {
|
||||||
|
match self {
|
||||||
|
Item::Event(e) => e.last_modified(),
|
||||||
|
Item::Task(t) => t.last_modified(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sync_status(&self) -> &SyncStatus {
|
pub fn sync_status(&self) -> &SyncStatus {
|
||||||
match self {
|
match self {
|
||||||
Item::Event(e) => e.sync_status(),
|
Item::Event(e) => e.sync_status(),
|
||||||
|
|
17
src/task.rs
17
src/task.rs
|
@ -1,5 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use crate::item::ItemId;
|
use crate::item::ItemId;
|
||||||
use crate::item::SyncStatus;
|
use crate::item::SyncStatus;
|
||||||
|
@ -17,6 +18,8 @@ pub struct Task {
|
||||||
|
|
||||||
/// The sync status of this item
|
/// The sync status of this item
|
||||||
sync_status: SyncStatus,
|
sync_status: SyncStatus,
|
||||||
|
/// The last time this item was modified
|
||||||
|
last_modified: DateTime<Utc>,
|
||||||
|
|
||||||
/// The display name of the task
|
/// The display name of the task
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -31,17 +34,19 @@ impl Task {
|
||||||
let new_item_id = ItemId::random(parent_calendar_id);
|
let new_item_id = ItemId::random(parent_calendar_id);
|
||||||
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();
|
||||||
Self::new_with_parameters(name, completed, new_uid, new_item_id, new_sync_status)
|
let new_last_modified = Utc::now();
|
||||||
|
Self::new_with_parameters(name, completed, new_uid, new_item_id, new_sync_status, new_last_modified)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) -> Self {
|
pub fn new_with_parameters(name: String, completed: bool, uid: String, id: ItemId, sync_status: SyncStatus, last_modified: DateTime<Utc>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
uid,
|
uid,
|
||||||
name,
|
name,
|
||||||
sync_status,
|
sync_status,
|
||||||
completed,
|
completed,
|
||||||
|
last_modified,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +55,7 @@ impl Task {
|
||||||
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.completed }
|
||||||
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 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
|
||||||
|
@ -77,10 +83,16 @@ impl Task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_last_modified(&mut self) {
|
||||||
|
self.last_modified = Utc::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Rename a task.
|
/// Rename a task.
|
||||||
/// This updates its "last modified" field
|
/// This updates its "last modified" field
|
||||||
pub fn set_name(&mut self, new_name: String) {
|
pub fn set_name(&mut self, new_name: String) {
|
||||||
self.update_sync_status();
|
self.update_sync_status();
|
||||||
|
self.update_last_modified();
|
||||||
self.name = new_name;
|
self.name = new_name;
|
||||||
}
|
}
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
|
@ -93,6 +105,7 @@ impl Task {
|
||||||
/// Set the completion status
|
/// Set the completion status
|
||||||
pub fn set_completed(&mut self, new_value: bool) {
|
pub fn set_completed(&mut self, new_value: bool) {
|
||||||
self.update_sync_status();
|
self.update_sync_status();
|
||||||
|
self.update_last_modified();
|
||||||
self.completed = new_value;
|
self.completed = new_value;
|
||||||
}
|
}
|
||||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||||
|
|
Loading…
Add table
Reference in a new issue