Items include a last modified (DTSTAMP) date

This commit is contained in:
daladim 2021-04-14 21:19:32 +02:00
parent e24fab2ccb
commit 8e35f4c579
7 changed files with 119 additions and 12 deletions

44
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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
} }

View file

@ -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);

View file

@ -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]

View file

@ -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(),

View file

@ -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")]