Compare commits

...

1 Commits

Author SHA1 Message Date
Ilya 644800907d Add events 4 years ago
  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. 205
      src/ical/parser.rs
  7. 93
      src/task.rs
  8. 44
      src/utils/mod.rs

1
examples/provider-sync.rs

@ -7,6 +7,7 @@ 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,24 +1,27 @@
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://my.server.com/remote.php/dav/files/john"; pub const URL: &str = "https://next.anshorei.me/remote.php/dav/files/test";
pub const USERNAME: &str = "username"; pub const USERNAME: &str = "test";
pub const PASSWORD: &str = "secret_password"; pub const PASSWORD: &str = "1ETsJD6BPTIMLhmVQcCK";
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/"; // pub const EXAMPLE_EXISTING_CALENDAR_URL: &str =
pub const EXAMPLE_CREATED_CALENDAR_URL: &str = "https://my.server.com/remote.php/dav/calendars/john/a_calendar_that_we_have_created/"; // "https://my.server.com/remote.php/dav/calendars/john/a_calendar_name/";
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);
@ -33,13 +36,14 @@ 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!("Depending on your RUST_LOG value, you may see more or less details about the progress."); println!(
"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,6 +16,19 @@ 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>
@ -23,7 +36,6 @@ 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>
@ -249,4 +261,3 @@ impl DavCalendar for RemoteCalendar {
Ok(()) Ok(())
} }
} }

103
src/event.rs

@ -1,27 +1,100 @@
//! Calendar events (iCal `VEVENT` items) //! Calendar events (iCal `VEVENT` items)
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
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;
/// TODO: implement `Event` one day. /// An event
/// This crate currently only supports tasks, not calendar events. #[derive(Clone, Debug, Serialize, Deserialize)]
#[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,
name: String,
/// The sync status of this item
sync_status: SyncStatus, 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,
/// 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() -> Self { pub fn new(
unimplemented!(); name: String,
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 {
unimplemented!(); &self.url
} }
pub fn uid(&self) -> &str { pub fn uid(&self) -> &str {
@ -33,15 +106,23 @@ impl Event {
} }
pub fn ical_prod_id(&self) -> &str { pub fn ical_prod_id(&self) -> &str {
unimplemented!() &self.ical_prod_id
} }
pub fn creation_date(&self) -> Option<&DateTime<Utc>> { pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
unimplemented!() self.creation_date.as_ref()
} }
pub fn last_modified(&self) -> &DateTime<Utc> { pub fn last_modified(&self) -> &DateTime<Utc> {
unimplemented!() &self.last_modified
}
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 {

205
src/ical/parser.rs

@ -2,26 +2,33 @@
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(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<Item, Box<dyn Error>> { pub fn parse(
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) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()), Err(err) => {
Ok(item) => item, return Err(
format!("Unable to parse iCal data for item {}: {}", item_url, err).into(),
)
} }
Ok(item) => item,
},
}; };
let ical_prod_id = extract_ical_prod_id(&parsed_item) let ical_prod_id = extract_ical_prod_id(&parsed_item)
@ -29,9 +36,93 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
.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(_) => { CurrentType::Event(event) => {
Item::Event(Event::new()) let mut name = None;
}, 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;
@ -39,13 +130,14 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
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
@ -53,7 +145,7 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
// "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
@ -66,11 +158,16 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
// "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.
@ -97,7 +194,13 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
}; };
let last_modified = match last_modified { let last_modified = match last_modified {
Some(dt) => dt, Some(dt) => dt,
None => return Err(format!("Missing DTSTAMP for item {}, but this is required by RFC5545", item_url).into()), None => {
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 => {
@ -105,15 +208,29 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
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 {
Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters)) false => 0f32,
}, 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());
@ -122,34 +239,38 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
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() value.as_ref().and_then(|s| {
.and_then(|s| {
parse_date_time(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),
@ -179,7 +300,6 @@ 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
@ -261,11 +381,17 @@ 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!(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.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!(task.last_modified(), &Utc.ymd(2021, 03, 21).and_hms(0, 16, 0)); assert_eq!(
task.last_modified(),
&Utc.ymd(2021, 03, 21).and_hms(0, 16, 0)
);
} }
#[test] #[test]
@ -274,11 +400,19 @@ 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(EXAMPLE_ICAL_COMPLETED, item_url.clone(), sync_status.clone()).unwrap(); let item = parse(
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!(task.completion_status(), &CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))); assert_eq!(
task.completion_status(),
&CompletionStatus::Completed(Some(Utc.ymd(2021, 04, 02).and_hms(8, 15, 57)))
);
} }
#[test] #[test]
@ -287,7 +421,12 @@ 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(EXAMPLE_ICAL_COMPLETED_WITHOUT_A_COMPLETION_DATE, item_url.clone(), sync_status.clone()).unwrap(); let item = parse(
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);

93
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,7 +62,6 @@ 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.
@ -74,24 +73,45 @@ impl Task {
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 { CompletionStatus::Uncompleted }; } else {
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(name, new_uid, new_url, new_completion_status, new_sync_status, new_creation_date, new_last_modified, ical_prod_id, extra_parameters) Self::new_with_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(name: String, uid: String, new_url: Url, pub fn new_with_parameters(
name: String,
uid: String,
new_url: Url,
completion_status: CompletionStatus, completion_status: CompletionStatus,
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>, completion_percent: f32,
ical_prod_id: String, extra_parameters: Vec<Property>, sync_status: SyncStatus,
) -> Self 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,
@ -100,16 +120,39 @@ impl Task {
} }
} }
pub fn url(&self) -> &Url { &self.url } pub fn url(&self) -> &Url {
pub fn uid(&self) -> &str { &self.uid } &self.url
pub fn name(&self) -> &str { &self.name } }
pub fn completed(&self) -> bool { self.completion_status.is_completed() } pub fn uid(&self) -> &str {
pub fn ical_prod_id(&self) -> &str { &self.ical_prod_id } &self.uid
pub fn sync_status(&self) -> &SyncStatus { &self.sync_status } }
pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified } pub fn name(&self) -> &str {
pub fn creation_date(&self) -> Option<&DateTime<Utc>> { self.creation_date.as_ref() } &self.name
pub fn completion_status(&self) -> &CompletionStatus { &self.completion_status } }
pub fn extra_parameters(&self) -> &[Property] { &self.extra_parameters } pub fn completed(&self) -> bool {
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 {
@ -137,7 +180,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;
}, }
} }
} }
@ -145,7 +188,6 @@ 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) {
@ -169,7 +211,10 @@ 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(&mut self, new_completion_status: CompletionStatus) { pub fn mock_remote_calendar_set_completion_status(
&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;
} }

44
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,14 +49,10 @@ 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( let mut xml_writer = minidom::quick_xml::Writer::new_with_indent(std::io::stdout(), 0x20, 4);
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]);
} }
@ -74,7 +70,7 @@ where
for (_, item) in map { for (_, item) in map {
print_task(item); print_task(item);
} }
}, }
} }
} }
} }
@ -92,7 +88,7 @@ where
for (url, version_tag) in map { for (url, version_tag) in map {
println!(" * {} (version {:?})", url, version_tag); println!(" * {} (version {:?})", url, version_tag);
} }
}, }
} }
} }
} }
@ -100,7 +96,14 @@ 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() { "✓" } else { " " }; let completion = if task.completed() {
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(_) => "=",
@ -108,12 +111,27 @@ pub fn print_task(item: &Item) {
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
@ -140,7 +158,6 @@ 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();
@ -149,7 +166,6 @@ 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