Compare commits

..

No commits in common. 'feature/events' and 'master' have entirely different histories.

  1. 1
      examples/provider-sync.rs
  2. 26
      examples/shared.rs
  3. 2
      src/calendar/mod.rs
  4. 15
      src/calendar/remote_calendar.rs
  5. 103
      src/event.rs
  6. 217
      src/ical/parser.rs
  7. 99
      src/task.rs
  8. 46
      src/utils/mod.rs

1
examples/provider-sync.rs

@ -7,7 +7,6 @@ use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::calendar::SupportedComponents; use kitchen_fridge::calendar::SupportedComponents;
use kitchen_fridge::Item; use kitchen_fridge::Item;
use kitchen_fridge::Task; use kitchen_fridge::Task;
use kitchen_fridge::Event;
use kitchen_fridge::task::CompletionStatus; use kitchen_fridge::task::CompletionStatus;
use kitchen_fridge::CalDavProvider; use kitchen_fridge::CalDavProvider;
use kitchen_fridge::traits::BaseCalendar; use kitchen_fridge::traits::BaseCalendar;

26
examples/shared.rs

@ -1,27 +1,24 @@
use std::path::Path; use std::path::Path;
use kitchen_fridge::cache::Cache;
use kitchen_fridge::client::Client; use kitchen_fridge::client::Client;
use kitchen_fridge::traits::CalDavSource; use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::CalDavProvider; use kitchen_fridge::CalDavProvider;
use kitchen_fridge::cache::Cache;
// TODO: change these values with yours // TODO: change these values with yours
pub const URL: &str = "https://next.anshorei.me/remote.php/dav/files/test"; pub const URL: &str = "https://my.server.com/remote.php/dav/files/john";
pub const USERNAME: &str = "test"; pub const USERNAME: &str = "username";
pub const PASSWORD: &str = "1ETsJD6BPTIMLhmVQcCK"; pub const PASSWORD: &str = "secret_password";
// pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/";
// "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/"; pub const EXAMPLE_CREATED_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/";
pub const EXAMPLE_CREATED_CALENDAR_URL: &str =
"https://next.anshorei.me/remote.php/dav/calendars/test/a_calendar_that_we_have_created/";
// https://next.anshorei.me/remote.php/dav
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str =
"https://next.anshorei.me/remote.php/dav/calendars/test/personal/";
fn main() { fn main() {
panic!("This file is not supposed to be executed"); panic!("This file is not supposed to be executed");
} }
/// Initializes a Provider, and run an initial sync from the server /// Initializes a Provider, and run an initial sync from the server
pub async fn initial_sync(cache_folder: &str) -> CalDavProvider { pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
let cache_path = Path::new(cache_folder); let cache_path = Path::new(cache_folder);
@ -36,14 +33,13 @@ pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
}; };
let mut provider = CalDavProvider::new(client, cache); let mut provider = CalDavProvider::new(client, cache);
let cals = provider.local().get_calendars().await.unwrap(); let cals = provider.local().get_calendars().await.unwrap();
println!("---- Local items, before sync -----"); println!("---- Local items, before sync -----");
kitchen_fridge::utils::print_calendar_list(&cals).await; kitchen_fridge::utils::print_calendar_list(&cals).await;
println!("Starting a sync..."); println!("Starting a sync...");
println!( println!("Depending on your RUST_LOG value, you may see more or less details about the progress.");
"Depending on your RUST_LOG value, you may see more or less details about the progress."
);
// Note that we could use sync_with_feedback() to have better and formatted feedback // Note that we could use sync_with_feedback() to have better and formatted feedback
if provider.sync().await == false { if provider.sync().await == false {
log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync."); log::warn!("Sync did not complete, see the previous log lines for more info. You can safely start a new sync.");

2
src/calendar/mod.rs

@ -69,7 +69,7 @@ pub enum SearchFilter {
// /// Return only completed tasks // /// Return only completed tasks
// CompletedTasks, // CompletedTasks,
// /// Return only calendar events // /// Return only calendar events
Events, // Events,
} }
impl Default for SearchFilter { impl Default for SearchFilter {

15
src/calendar/remote_calendar.rs

@ -16,19 +16,6 @@ use crate::item::SyncStatus;
use crate::resource::Resource; use crate::resource::Resource;
use crate::utils::find_elem; use crate::utils::find_elem;
// static TASKS_BODY: &str = r#"
// <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
// <d:prop>
// <d:getetag />
// </d:prop>
// <c:filter>
// <c:comp-filter name="VCALENDAR">
// <c:comp-filter name="VTODO" />
// </c:comp-filter>
// </c:filter>
// </c:calendar-query>
// "#;
static TASKS_BODY: &str = r#" static TASKS_BODY: &str = r#"
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"> <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop> <d:prop>
@ -36,6 +23,7 @@ static TASKS_BODY: &str = r#"
</d:prop> </d:prop>
<c:filter> <c:filter>
<c:comp-filter name="VCALENDAR"> <c:comp-filter name="VCALENDAR">
<c:comp-filter name="VTODO" />
</c:comp-filter> </c:comp-filter>
</c:filter> </c:filter>
</c:calendar-query> </c:calendar-query>
@ -261,3 +249,4 @@ impl DavCalendar for RemoteCalendar {
Ok(()) Ok(())
} }
} }

103
src/event.rs

@ -1,100 +1,27 @@
//! Calendar events (iCal `VEVENT` items) //! Calendar events (iCal `VEVENT` items)
use chrono::{DateTime, Utc};
use ical::property::Property;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use url::Url; use url::Url;
use uuid::Uuid;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::utils::random_url;
/// An event /// TODO: implement `Event` one day.
#[derive(Clone, Debug, Serialize, Deserialize)] /// This crate currently only supports tasks, not calendar events.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Event { pub struct Event {
/// The event URL
url: Url,
/// Persistent, globally unique identifier for the calendar component.
uid: String, uid: String,
/// The sync status of this item
sync_status: SyncStatus,
/// The time this event was created.
/// This is not required by RFC5545. This will be populated in events created by this crate, but can be None for events coming from a server
creation_date: Option<DateTime<Utc>>,
last_modified: DateTime<Utc>,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
/// The display name of the event
name: String, name: String,
sync_status: SyncStatus,
/// The PRODID, as defined in iCal files
ical_prod_id: String,
/// Extra parameters that have not been parsed from the iCal file (because they're not supported (yet) by this crate).
/// They are needed to serialize this item into an equivalent iCal file
extra_parameters: Vec<Property>,
} }
impl Event { impl Event {
pub fn new( pub fn new() -> Self {
name: String, unimplemented!();
parent_calendar_url: &Url,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
) -> Self {
let new_url = random_url(parent_calendar_url);
let new_sync_status = SyncStatus::NotSynced;
let new_uid = Uuid::new_v4().to_hyphenated().to_string();
let new_creation_date = Some(Utc::now());
let new_last_modified = Utc::now();
let ical_prod_id = crate::ical::default_prod_id();
let extra_parameters = Vec::new();
Self::new_with_parameters(
name,
new_uid,
new_url,
new_sync_status,
new_creation_date,
new_last_modified,
start_date,
end_date,
ical_prod_id,
extra_parameters,
)
}
pub fn new_with_parameters(
name: String,
uid: String,
new_url: Url,
sync_status: SyncStatus,
creation_date: Option<DateTime<Utc>>,
last_modified: DateTime<Utc>,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
ical_prod_id: String,
extra_parameters: Vec<Property>,
) -> Self {
Self {
url: new_url,
uid,
name,
sync_status,
creation_date,
last_modified,
start_date,
end_date,
ical_prod_id,
extra_parameters,
}
} }
pub fn url(&self) -> &Url { pub fn url(&self) -> &Url {
&self.url unimplemented!();
} }
pub fn uid(&self) -> &str { pub fn uid(&self) -> &str {
@ -106,23 +33,15 @@ impl Event {
} }
pub fn ical_prod_id(&self) -> &str { pub fn ical_prod_id(&self) -> &str {
&self.ical_prod_id unimplemented!()
} }
pub fn creation_date(&self) -> Option<&DateTime<Utc>> { pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
self.creation_date.as_ref() unimplemented!()
} }
pub fn last_modified(&self) -> &DateTime<Utc> { pub fn last_modified(&self) -> &DateTime<Utc> {
&self.last_modified unimplemented!()
}
pub fn start_date(&self) -> &DateTime<Utc> {
&self.start_date
}
pub fn end_date(&self) -> &DateTime<Utc> {
&self.end_date
} }
pub fn sync_status(&self) -> &SyncStatus { pub fn sync_status(&self) -> &SyncStatus {

217
src/ical/parser.rs

@ -2,33 +2,26 @@
use std::error::Error; use std::error::Error;
use chrono::{Date, DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo}; use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
use chrono::{DateTime, TimeZone, Utc};
use url::Url; use url::Url;
use crate::Item;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::Task;
use crate::task::CompletionStatus; use crate::task::CompletionStatus;
use crate::Event; use crate::Event;
use crate::Item;
use crate::Task;
/// Parse an iCal file into the internal representation [`crate::Item`] /// Parse an iCal file into the internal representation [`crate::Item`]
pub fn parse( pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> {
content: &str,
item_url: Url,
sync_status: SyncStatus,
) -> Result<Item, Box<dyn Error>> {
let mut reader = ical::IcalParser::new(content.as_bytes()); let mut reader = ical::IcalParser::new(content.as_bytes());
let parsed_item = match reader.next() { let parsed_item = match reader.next() {
None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()), None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()),
Some(item) => match item { Some(item) => match item {
Err(err) => { Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()),
return Err(
format!("Unable to parse iCal data for item {}: {}", item_url, err).into(),
)
}
Ok(item) => item, Ok(item) => item,
}, }
}; };
let ical_prod_id = extract_ical_prod_id(&parsed_item) let ical_prod_id = extract_ical_prod_id(&parsed_item)
@ -36,93 +29,9 @@ pub fn parse(
.unwrap_or_else(|| super::default_prod_id()); .unwrap_or_else(|| super::default_prod_id());
let item = match assert_single_type(&parsed_item)? { let item = match assert_single_type(&parsed_item)? {
CurrentType::Event(event) => { CurrentType::Event(_) => {
let mut name = None; Item::Event(Event::new())
let mut uid = None; },
let mut last_modified = None;
let mut creation_date = None;
let mut start_date = None;
let mut end_date = None;
let mut extra_parameters = Vec::new();
for prop in &event.properties {
match prop.name.as_str() {
"SUMMARY" => name = prop.value.clone(),
"UID" => uid = prop.value.clone(),
"DTSTART" => {
start_date = parse_date_time_from_property(&prop.value);
}
"DTEND" => {
end_date = parse_date_time_from_property(&prop.value);
}
"DTSTAMP" => {
last_modified = parse_date_time_from_property(&prop.value);
}
"LAST-MODIFIED" => {
last_modified = parse_date_time_from_property(&prop.value);
}
"CREATED" => {
// The property can be specified once, but is not mandatory
creation_date = parse_date_time_from_property(&prop.value)
}
_ => {
// This field is not supported. Let's store it anyway, so that we are able to re-create an identical iCal file
extra_parameters.push(prop.clone());
}
}
}
let name = match name {
Some(name) => name,
None => return Err(format!("Missing name for item {}", item_url).into()),
};
let uid = match uid {
Some(uid) => uid,
None => return Err(format!("Missing UID for item {}", item_url).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_url
)
.into())
}
};
let start_date = match start_date {
Some(dt) => dt,
None => {
return Err(format!(
"Missing DTSTART for item {}, but this is required by RFC5545",
item_url
)
.into())
}
};
let end_date = match end_date {
Some(dt) => dt,
None => {
return Err(format!(
"Missing DTEND for item {}, but this is required by RFC5545",
item_url
)
.into())
}
};
Item::Event(Event::new_with_parameters(
name,
uid,
item_url,
sync_status,
creation_date,
last_modified,
start_date,
end_date,
ical_prod_id,
extra_parameters,
))
}
CurrentType::Todo(todo) => { CurrentType::Todo(todo) => {
let mut name = None; let mut name = None;
@ -130,14 +39,13 @@ pub fn parse(
let mut completed = false; let mut completed = false;
let mut last_modified = None; let mut last_modified = None;
let mut completion_date = None; let mut completion_date = None;
let mut completion_percent = None;
let mut creation_date = None; let mut creation_date = None;
let mut extra_parameters = Vec::new(); let mut extra_parameters = Vec::new();
for prop in &todo.properties { for prop in &todo.properties {
match prop.name.as_str() { match prop.name.as_str() {
"SUMMARY" => name = prop.value.clone(), "SUMMARY" => { name = prop.value.clone() },
"UID" => uid = prop.value.clone(), "UID" => { uid = prop.value.clone() },
"DTSTAMP" => { "DTSTAMP" => {
// The property can be specified once, but is not mandatory // The property can be specified once, but is not mandatory
// "This property specifies the date and time that the information associated with // "This property specifies the date and time that the information associated with
@ -145,7 +53,7 @@ pub fn parse(
// "In the case of an iCalendar object that doesn't specify a "METHOD" // "In the case of an iCalendar object that doesn't specify a "METHOD"
// property [e.g.: VTODO and VEVENT], this property is equivalent to the "LAST-MODIFIED" property". // property [e.g.: VTODO and VEVENT], this property is equivalent to the "LAST-MODIFIED" property".
last_modified = parse_date_time_from_property(&prop.value); last_modified = parse_date_time_from_property(&prop.value);
} },
"LAST-MODIFIED" => { "LAST-MODIFIED" => {
// The property can be specified once, but is not mandatory // The property can be specified once, but is not mandatory
// "This property specifies the date and time that the information associated with // "This property specifies the date and time that the information associated with
@ -158,16 +66,11 @@ pub fn parse(
// "This property defines the date and time that a to-do was // "This property defines the date and time that a to-do was
// actually completed." // actually completed."
completion_date = parse_date_time_from_property(&prop.value) completion_date = parse_date_time_from_property(&prop.value)
} },
"PERCENT-COMPLETE" => {
if let Some(value) = &prop.value {
completion_percent = value.parse().ok()
}
}
"CREATED" => { "CREATED" => {
// The property can be specified once, but is not mandatory // The property can be specified once, but is not mandatory
creation_date = parse_date_time_from_property(&prop.value) creation_date = parse_date_time_from_property(&prop.value)
} },
"STATUS" => { "STATUS" => {
// Possible values: // Possible values:
// "NEEDS-ACTION" ;Indicates to-do needs action. // "NEEDS-ACTION" ;Indicates to-do needs action.
@ -194,13 +97,7 @@ pub fn parse(
}; };
let last_modified = match last_modified { let last_modified = match last_modified {
Some(dt) => dt, Some(dt) => dt,
None => { None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()),
return Err(format!(
"Missing DTSTAMP for item {}, but this is required by RFC5545",
item_url
)
.into())
}
}; };
let completion_status = match completed { let completion_status = match completed {
false => { false => {
@ -208,29 +105,15 @@ pub fn parse(
log::warn!("Task {:?} has an inconsistent content: its STATUS is not completed, yet it has a COMPLETED timestamp at {:?}", uid, completion_date); log::warn!("Task {:?} has an inconsistent content: its STATUS is not completed, yet it has a COMPLETED timestamp at {:?}", uid, completion_date);
} }
CompletionStatus::Uncompleted CompletionStatus::Uncompleted
} },
true => CompletionStatus::Completed(completion_date), true => CompletionStatus::Completed(completion_date),
}; };
let completion_percent = completion_percent.unwrap_or(match completed {
false => 0f32, Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters))
true => 100f32, },
});
Item::Task(Task::new_with_parameters(
name,
uid,
item_url,
completion_status,
completion_percent,
sync_status,
creation_date,
last_modified,
ical_prod_id,
extra_parameters,
))
}
}; };
// What to do with multiple items? // What to do with multiple items?
if reader.next().map(|r| r.is_ok()) == Some(true) { if reader.next().map(|r| r.is_ok()) == Some(true) {
return Err("Parsing multiple items are not supported".into()); return Err("Parsing multiple items are not supported".into());
@ -239,38 +122,34 @@ pub fn parse(
Ok(item) Ok(item)
} }
fn parse_date(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
NaiveDate::parse_from_str("20220413", "%Y%m%d")
.map(|dt| dt.and_time(NaiveTime::from_hms(0, 0, 0)))
.map(|dt| DateTime::from_utc(dt, Utc))
}
fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> { fn parse_date_time(dt: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ") Utc.datetime_from_str(dt, "%Y%m%dT%H%M%SZ")
.or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S")) .or_else(|_err| Utc.datetime_from_str(dt, "%Y%m%dT%H%M%S") )
.or_else(|_err| parse_date(dt))
} }
fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> { fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> {
value.as_ref().and_then(|s| { value.as_ref()
parse_date_time(s) .and_then(|s| {
parse_date_time(s)
.map_err(|err| { .map_err(|err| {
log::warn!("Invalid timestamp: '{}'", s); log::warn!("Invalid timestamp: {}", s);
err err
}) })
.ok() .ok()
}) })
} }
fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> { fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> {
for prop in &item.properties { for prop in &item.properties {
if &prop.name == "PRODID" { if &prop.name == "PRODID" {
return prop.value.as_ref().map(|s| s.as_str()); return prop.value.as_ref().map(|s| s.as_str())
} }
} }
None None
} }
enum CurrentType<'a> { enum CurrentType<'a> {
Event(&'a IcalEvent), Event(&'a IcalEvent),
Todo(&'a IcalTodo), Todo(&'a IcalTodo),
@ -300,6 +179,7 @@ fn assert_single_type<'a>(item: &'a IcalCalendar) -> Result<CurrentType<'a>, Box
return Err("Only a single TODO or a single EVENT is supported".into()); return Err("Only a single TODO or a single EVENT is supported".into());
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR
@ -315,7 +195,7 @@ END:VTODO
END:VCALENDAR END:VCALENDAR
"#; "#;
const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//Nextcloud Tasks v0.13.6 PRODID:-//Nextcloud Tasks v0.13.6
BEGIN:VTODO BEGIN:VTODO
@ -331,7 +211,7 @@ END:VTODO
END:VCALENDAR END:VCALENDAR
"#; "#;
const EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE: &str = r#"BEGIN:VCALENDAR const EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE: &str = r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//Nextcloud Tasks v0.13.6 PRODID:-//Nextcloud Tasks v0.13.6
BEGIN:VTODO BEGIN:VTODO
@ -381,17 +261,11 @@ END:VCALENDAR
assert_eq!(task.name(), "Do not forget to do this"); assert_eq!(task.name(), "Do not forget to do this");
assert_eq!(task.url(), &item_url); assert_eq!(task.url(), &item_url);
assert_eq!( assert_eq!(task.uid(), "0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com");
task.uid(),
"0633de27-8c32-42be-bcb8-63bc879c6185@some-domain.com"
);
assert_eq!(task.completed(), false); assert_eq!(task.completed(), false);
assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted); assert_eq!(task.completion_status(), &CompletionStatus::Uncompleted);
assert_eq!(task.sync_status(), &sync_status); assert_eq!(task.sync_status(), &sync_status);
assert_eq!( assert_eq!(task.last_modified(), &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0));
task.last_modified(),
&Utc.ymd(2021, 03, 21).and_hms(0, 16, 0)
);
} }
#[test] #[test]
@ -400,19 +274,11 @@ END:VCALENDAR
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_url: Url = "http://some.id/for/testing".parse().unwrap(); let item_url: Url = "http://some.id/for/testing".parse().unwrap();
let item = parse( let item = parse(EXAMPLE_ICAL_COMPLETED, item_url.clone(), sync_status.clone()).unwrap();
EXAMPLE_ICAL_COMPLETED,
item_url.clone(),
sync_status.clone(),
)
.unwrap();
let task = item.unwrap_task(); let task = item.unwrap_task();
assert_eq!(task.completed(), true); assert_eq!(task.completed(), true);
assert_eq!( assert_eq!(task.completion_status(), &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57))));
task.completion_status(),
&CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))
);
} }
#[test] #[test]
@ -421,12 +287,7 @@ END:VCALENDAR
let sync_status = SyncStatus::Synced(version_tag); let sync_status = SyncStatus::Synced(version_tag);
let item_url: Url = "http://some.id/for/testing".parse().unwrap(); let item_url: Url = "http://some.id/for/testing".parse().unwrap();
let item = parse( let item = parse(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_url.clone(), sync_status.clone()).unwrap();
EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE,
item_url.clone(),
sync_status.clone(),
)
.unwrap();
let task = item.unwrap_task(); let task = item.unwrap_task();
assert_eq!(task.completed(), true); assert_eq!(task.completed(), true);

99
src/task.rs

@ -1,10 +1,10 @@
//! To-do tasks (iCal `VTODO` item) //! To-do tasks (iCal `VTODO` item)
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ical::property::Property; use ical::property::Property;
use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use uuid::Uuid;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::utils::random_url; use crate::utils::random_url;
@ -49,11 +49,11 @@ pub struct Task {
last_modified: DateTime<Utc>, last_modified: DateTime<Utc>,
/// The completion status of this task /// The completion status of this task
completion_status: CompletionStatus, completion_status: CompletionStatus,
completion_percent: f32,
/// The display name of the task /// The display name of the task
name: String, name: String,
/// The PRODID, as defined in iCal files /// The PRODID, as defined in iCal files
ical_prod_id: String, ical_prod_id: String,
@ -62,6 +62,7 @@ pub struct Task {
extra_parameters: Vec<Property>, extra_parameters: Vec<Property>,
} }
impl Task { impl Task {
/// Create a brand new Task that is not on a server yet. /// Create a brand new Task that is not on a server yet.
/// This will pick a new (random) task ID. /// This will pick a new (random) task ID.
@ -72,46 +73,25 @@ impl Task {
let new_creation_date = Some(Utc::now()); let new_creation_date = Some(Utc::now());
let new_last_modified = Utc::now(); let new_last_modified = Utc::now();
let new_completion_status = if completed { let new_completion_status = if completed {
CompletionStatus::Completed(Some(Utc::now())) CompletionStatus::Completed(Some(Utc::now()))
} else { } else { CompletionStatus::Uncompleted };
CompletionStatus::Uncompleted
};
let new_completion_percent = if completed { 100f32 } else { 0f32 };
let ical_prod_id = crate::ical::default_prod_id(); let ical_prod_id = crate::ical::default_prod_id();
let extra_parameters = Vec::new(); let extra_parameters = Vec::new();
Self::new_with_parameters( Self::new_with_parameters(name, new_uid, new_url, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters)
name,
new_uid,
new_url,
new_completion_status,
new_completion_percent,
new_sync_status,
new_creation_date,
new_last_modified,
ical_prod_id,
extra_parameters,
)
} }
/// Create a new Task instance, that may be synced on the server already /// Create a new Task instance, that may be synced on the server already
pub fn new_with_parameters( pub fn new_with_parameters(name: String, uid: String, new_url: Url,
name: String, completion_status: CompletionStatus,
uid: String, sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>,
new_url: Url, ical_prod_id: String, extra_parameters: Vec<Property>,
completion_status: CompletionStatus, ) -> Self
completion_percent: f32, {
sync_status: SyncStatus,
creation_date: Option<DateTime<Utc>>,
last_modified: DateTime<Utc>,
ical_prod_id: String,
extra_parameters: Vec<Property>,
) -> Self {
Self { Self {
url: new_url, url: new_url,
uid, uid,
name, name,
completion_status, completion_status,
completion_percent,
sync_status, sync_status,
creation_date, creation_date,
last_modified, last_modified,
@ -120,43 +100,20 @@ impl Task {
} }
} }
pub fn url(&self) -> &Url { pub fn url(&self) -> &Url { &self.url }
&self.url pub fn uid(&self) -> &str { &self.uid }
} pub fn name(&self) -> &str { &self.name }
pub fn uid(&self) -> &str { pub fn completed(&self) -> bool { self.completion_status.is_completed() }
&self.uid pub fn ical_prod_id(&self) -> &str { &self.ical_prod_id }
} pub fn sync_status(&self) -> &SyncStatus { &self.sync_status }
pub fn name(&self) -> &str { pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified }
&self.name pub fn creation_date(&self) -> Option<&DateTime<Utc>> { self.creation_date.as_ref() }
} pub fn completion_status(&self) -> &CompletionStatus { &self.completion_status }
pub fn completed(&self) -> bool { pub fn extra_parameters(&self) -> &[Property] { &self.extra_parameters }
self.completion_status.is_completed()
}
pub fn completion_percent(&self) -> f32 {
self.completion_percent
}
pub fn ical_prod_id(&self) -> &str {
&self.ical_prod_id
}
pub fn sync_status(&self) -> &SyncStatus {
&self.sync_status
}
pub fn last_modified(&self) -> &DateTime<Utc> {
&self.last_modified
}
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
self.creation_date.as_ref()
}
pub fn completion_status(&self) -> &CompletionStatus {
&self.completion_status
}
pub fn extra_parameters(&self) -> &[Property] {
&self.extra_parameters
}
#[cfg(any(test, feature = "integration_tests"))] #[cfg(any(test, feature = "integration_tests"))]
pub fn has_same_observable_content_as(&self, other: &Task) -> bool { pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
self.url == other.url self.url == other.url
&& self.uid == other.uid && self.uid == other.uid
&& self.name == other.name && self.name == other.name
// 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
@ -180,7 +137,7 @@ impl Task {
SyncStatus::LocallyDeleted(_) => { SyncStatus::LocallyDeleted(_) => {
log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync."); log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync.");
return; return;
} },
} }
} }
@ -188,6 +145,7 @@ impl Task {
self.last_modified = Utc::now(); 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) {
@ -211,10 +169,7 @@ impl Task {
} }
#[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_completion_status( pub fn mock_remote_calendar_set_completion_status(&mut self, new_completion_status: CompletionStatus) {
&mut self,
new_completion_status: CompletionStatus,
) {
self.sync_status = SyncStatus::random_synced(); self.sync_status = SyncStatus::random_synced();
self.completion_status = new_completion_status; self.completion_status = new_completion_status;
} }

46
src/utils/mod.rs

@ -1,17 +1,17 @@
//! Some utility functions //! Some utility functions
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::hash::Hash; use std::hash::Hash;
use std::io::{stdin, stdout, Read, Write}; use std::io::{stdin, stdout, Read, Write};
use std::sync::{Arc, Mutex};
use minidom::Element; use minidom::Element;
use url::Url; use url::Url;
use crate::item::SyncStatus;
use crate::traits::CompleteCalendar; use crate::traits::CompleteCalendar;
use crate::traits::DavCalendar; use crate::traits::DavCalendar;
use crate::Item; use crate::Item;
use crate::item::SyncStatus;
/// Walks an XML tree and returns every element that has the given name /// Walks an XML tree and returns every element that has the given name
pub fn find_elems<S: AsRef<str>>(root: &Element, searched_name: S) -> Vec<&Element> { pub fn find_elems<S: AsRef<str>>(root: &Element, searched_name: S) -> Vec<&Element> {
@ -49,10 +49,14 @@ pub fn find_elem<S: AsRef<str>>(root: &Element, searched_name: S) -> Option<&Ele
None None
} }
pub fn print_xml(element: &Element) { pub fn print_xml(element: &Element) {
let mut writer = std::io::stdout(); let mut writer = std::io::stdout();
let mut xml_writer = minidom::quick_xml::Writer::new_with_indent(std::io::stdout(), 0x20, 4); let mut xml_writer = minidom::quick_xml::Writer::new_with_indent(
std::io::stdout(),
0x20, 4
);
let _ = element.to_writer(&mut xml_writer); let _ = element.to_writer(&mut xml_writer);
let _ = writer.write(&[0x0a]); let _ = writer.write(&[0x0a]);
} }
@ -70,7 +74,7 @@ where
for (_, item) in map { for (_, item) in map {
print_task(item); print_task(item);
} }
} },
} }
} }
} }
@ -88,7 +92,7 @@ where
for (url, version_tag) in map { for (url, version_tag) in map {
println!(" * {} (version {:?})", url, version_tag); println!(" * {} (version {:?})", url, version_tag);
} }
} },
} }
} }
} }
@ -96,42 +100,20 @@ where
pub fn print_task(item: &Item) { pub fn print_task(item: &Item) {
match item { match item {
Item::Task(task) => { Item::Task(task) => {
let completion = if task.completed() { let completion = if task.completed() { "✓" } else { " " };
String::from("✓")
} else if task.completion_percent() > 0f32 {
format!("{}%", task.completion_percent())
} else {
String::new()
};
let completion = format!("{completion:>width$}", completion = completion, width = 4);
let sync = match task.sync_status() { let sync = match task.sync_status() {
SyncStatus::NotSynced => ".", SyncStatus::NotSynced => ".",
SyncStatus::Synced(_) => "=", SyncStatus::Synced(_) => "=",
SyncStatus::LocallyModified(_) => "~", SyncStatus::LocallyModified(_) => "~",
SyncStatus::LocallyDeleted(_) => "x", SyncStatus::LocallyDeleted(_) => "x",
}; };
println!(" {}{} {}\t{}", completion, sync, task.name(), task.url()); println!(" {}{} {}\t{}", completion, sync, task.name(), task.url());
} },
Item::Event(event) => {
let sync = match event.sync_status() {
SyncStatus::NotSynced => ".",
SyncStatus::Synced(_) => "=",
SyncStatus::LocallyModified(_) => "~",
SyncStatus::LocallyDeleted(_) => "x",
};
println!(
" {}-{}{} {}\t{}",
event.start_date(),
event.end_date(),
sync,
event.name(),
event.url()
)
}
_ => return, _ => return,
} }
} }
/// Compare keys of two hashmaps for equality /// Compare keys of two hashmaps for equality
pub fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool pub fn keys_are_the_same<T, U, V>(left: &HashMap<T, U>, right: &HashMap<T, V>) -> bool
where where
@ -158,6 +140,7 @@ where
result result
} }
/// Wait for the user to press enter /// Wait for the user to press enter
pub fn pause() { pub fn pause() {
let mut stdout = stdout(); let mut stdout = stdout();
@ -166,6 +149,7 @@ pub fn pause() {
stdin().read_exact(&mut [0]).unwrap(); stdin().read_exact(&mut [0]).unwrap();
} }
/// Generate a random URL with a given prefix /// Generate a random URL with a given prefix
pub fn random_url(parent_calendar: &Url) -> Url { pub fn random_url(parent_calendar: &Url) -> Url {
let random = uuid::Uuid::new_v4().to_hyphenated().to_string(); let random = uuid::Uuid::new_v4().to_hyphenated().to_string();

Loading…
Cancel
Save