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"
|
||||
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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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(),
|
||||
|
|
17
src/task.rs
17
src/task.rs
|
@ -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")]
|
||||
|
|
Loading…
Add table
Reference in a new issue