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"
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]]
name = "core-foundation"
version = "0.9.1"
@ -437,6 +451,7 @@ version = "0.1.0"
dependencies = [
"async-trait",
"bitflags",
"chrono",
"env_logger",
"ical",
"ics",
@ -478,6 +493,25 @@ dependencies = [
"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]]
name = "num_cpus"
version = "1.13.0"
@ -886,6 +920,16 @@ dependencies = [
"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]]
name = "tinyvec"
version = "1.1.1"

View file

@ -25,3 +25,4 @@ uuid = { version = "0.8", features = ["v4"] }
sanitize-filename = "0.3"
ical = "0.7"
ics = "0.5"
chrono = { version = "0.4", features = ["serde"] }

View file

@ -1,6 +1,7 @@
//! Calendar events
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use crate::item::ItemId;
use crate::item::SyncStatus;
@ -31,6 +32,10 @@ impl Event {
&self.name
}
pub fn last_modified(&self) -> &DateTime<Utc> {
unimplemented!()
}
pub fn sync_status(&self) -> &SyncStatus {
&self.sync_status
}

View file

@ -2,7 +2,8 @@
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 crate::item::Item;
@ -14,9 +15,14 @@ fn ical_product_id() -> String {
/// Create an iCal item from a `crate::item::Item`
pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
let mut todo = ToDo::new(item.uid(), "20181021T190000");
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 s_last_modified = format_date_time(item.last_modified());
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 {
Item::Task(t) => {
@ -34,6 +40,9 @@ pub fn build_from(item: &Item) -> Result<String, Box<dyn Error>> {
Ok(calendar.to_string())
}
fn format_date_time(dt: &DateTime<Utc>) -> String {
dt.format("%Y%m%dT%H%M%S").to_string()
}
#[cfg(test)]
@ -44,20 +53,21 @@ 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 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\
VERSION:2.0\r\n\
PRODID:-//{}//{}//EN\r\n\
BEGIN:VTODO\r\n\
UID:{}\r\n\
DTSTAMP:20181021T190000\r\n\
SUMMARY:Take pictures of squirrels (with ÜTF-8 chars)\r\n\
COMMENT:That's really something I'd like to do one day\r\n\
DTSTAMP:{}\r\n\
SUMMARY:This is a task with ÜTF-8 characters\r\n\
STATUS:COMPLETED\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);
assert_eq!(ical.unwrap(), expected_ical);

View file

@ -3,6 +3,7 @@
use std::error::Error;
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
use chrono::{DateTime, TimeZone, Utc};
use crate::Item;
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 uid = None;
let mut completed = false;
let mut last_modified = None;
for prop in &todo.properties {
if prop.name == "SUMMARY" {
name = prop.value.clone();
@ -48,6 +50,19 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
if prop.name == "UID" {
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 {
Some(name) => name,
@ -57,8 +72,12 @@ pub fn parse(content: &str, item_id: ItemId, sync_status: SyncStatus) -> Result<
Some(uid) => uid,
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)
}
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> {
Event(&'a IcalEvent),
Todo(&'a IcalTodo),
@ -171,6 +196,7 @@ END:VCALENDAR
assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
assert_eq!(task.completed(), false);
assert_eq!(task.sync_status(), &sync_status);
assert_eq!(task.last_modified(), &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0));
}
#[test]

View file

@ -5,6 +5,7 @@ use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use url::Url;
use chrono::{DateTime, Utc};
use crate::resource::Resource;
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 {
match self {
Item::Event(e) => e.sync_status(),

View file

@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::item::ItemId;
use crate::item::SyncStatus;
@ -17,6 +18,8 @@ pub struct Task {
/// The sync status of this item
sync_status: SyncStatus,
/// The last time this item was modified
last_modified: DateTime<Utc>,
/// The display name of the task
name: String,
@ -31,17 +34,19 @@ impl Task {
let new_item_id = ItemId::random(parent_calendar_id);
let new_sync_status = SyncStatus::NotSynced;
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
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 {
id,
uid,
name,
sync_status,
completed,
last_modified,
}
}
@ -50,6 +55,7 @@ impl Task {
pub fn name(&self) -> &str { &self.name }
pub fn completed(&self) -> bool { self.completed }
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 {
self.id == other.id
@ -77,10 +83,16 @@ impl Task {
}
}
fn update_last_modified(&mut self) {
self.last_modified = Utc::now();
}
/// Rename a task.
/// This updates its "last modified" field
pub fn set_name(&mut self, new_name: String) {
self.update_sync_status();
self.update_last_modified();
self.name = new_name;
}
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
@ -93,6 +105,7 @@ impl Task {
/// Set the completion status
pub fn set_completed(&mut self, new_value: bool) {
self.update_sync_status();
self.update_last_modified();
self.completed = new_value;
}
#[cfg(feature = "local_calendar_mocks_remote_calendars")]