Browse Source

Add events

feature/events
Ilya 4 years ago
parent
commit
644800907d
  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,6 +7,7 @@ use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::calendar::SupportedComponents;
use kitchen_fridge::Item;
use kitchen_fridge::Task;
use kitchen_fridge::Event;
use kitchen_fridge::task::CompletionStatus;
use kitchen_fridge::CalDavProvider;
use kitchen_fridge::traits::BaseCalendar;

26
examples/shared.rs

@ -1,24 +1,27 @@
use std::path::Path;
use kitchen_fridge::cache::Cache;
use kitchen_fridge::client::Client;
use kitchen_fridge::traits::CalDavSource;
use kitchen_fridge::CalDavProvider;
use kitchen_fridge::cache::Cache;
// TODO: change these values with yours
pub const URL: &str = "https://my.server.com/remote.php/dav/files/john";
pub const USERNAME: &str = "username";
pub const PASSWORD: &str = "secret_password";
pub const EXAMPLE_EXISTING_CALENDAR_URL: &str = "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 URL: &str = "https://next.anshorei.me/remote.php/dav/files/test";
pub const USERNAME: &str = "test";
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_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() {
panic!("This file is not supposed to be executed");
}
/// Initializes a Provider, and run an initial sync from the server
pub async fn initial_sync(cache_folder: &str) -> CalDavProvider {
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 cals = provider.local().get_calendars().await.unwrap();
println!("---- Local items, before sync -----");
kitchen_fridge::utils::print_calendar_list(&cals).await;
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
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.");

2
src/calendar/mod.rs

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

15
src/calendar/remote_calendar.rs

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

103
src/event.rs

@ -1,27 +1,100 @@
//! Calendar events (iCal `VEVENT` items)
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use ical::property::Property;
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use crate::item::SyncStatus;
use crate::utils::random_url;
/// TODO: implement `Event` one day.
/// This crate currently only supports tasks, not calendar events.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// An event
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Event {
/// The event URL
url: Url,
/// Persistent, globally unique identifier for the calendar component.
uid: String,
name: 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,
/// 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 {
pub fn new() -> Self {
unimplemented!();
pub fn new(
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 {
unimplemented!();
&self.url
}
pub fn uid(&self) -> &str {
@ -33,15 +106,23 @@ impl Event {
}
pub fn ical_prod_id(&self) -> &str {
unimplemented!()
&self.ical_prod_id
}
pub fn creation_date(&self) -> Option<&DateTime<Utc>> {
unimplemented!()
self.creation_date.as_ref()
}
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 {

217
src/ical/parser.rs

@ -2,26 +2,33 @@
use std::error::Error;
use chrono::{Date, DateTime, NaiveDate, NaiveTime, TimeZone, Utc};
use ical::parser::ical::component::{IcalCalendar, IcalEvent, IcalTodo};
use chrono::{DateTime, TimeZone, Utc};
use url::Url;
use crate::Item;
use crate::item::SyncStatus;
use crate::Task;
use crate::task::CompletionStatus;
use crate::Event;
use crate::Item;
use crate::Task;
/// 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 parsed_item = match reader.next() {
None => return Err(format!("Invalid iCal data to parse for item {}", item_url).into()),
Some(item) => match item {
Err(err) => return Err(format!("Unable to parse iCal data for item {}: {}", item_url, err).into()),
Err(err) => {
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)
@ -29,9 +36,93 @@ pub fn parse(content: &str, item_url: Url, sync_status: SyncStatus) -> Result<It
.unwrap_or_else(|| super::default_prod_id());
let item = match assert_single_type(&parsed_item)? {
CurrentType::Event(_) => {
Item::Event(Event::new())
},
CurrentType::Event(event) => {
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) => {
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 last_modified = None;
let mut completion_date = None;
let mut completion_percent = None;
let mut creation_date = None;
let mut extra_parameters = Vec::new();
for prop in &todo.properties {
match prop.name.as_str() {
"SUMMARY" => { name = prop.value.clone() },
"UID" => { uid = prop.value.clone() },
"SUMMARY" => name = prop.value.clone(),
"UID" => uid = prop.value.clone(),
"DTSTAMP" => {
// The property can be specified once, but is not mandatory
// "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"
// 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" => {
// The property can be specified once, but is not mandatory
// "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
// actually completed."
completion_date = parse_date_time_from_property(&prop.value)
},
}
"PERCENT-COMPLETE" => {
if let Some(value) = &prop.value {
completion_percent = value.parse().ok()
}
}
"CREATED" => {
// The property can be specified once, but is not mandatory
creation_date = parse_date_time_from_property(&prop.value)
},
}
"STATUS" => {
// Possible values:
// "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 {
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 {
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);
}
CompletionStatus::Uncompleted
},
}
true => CompletionStatus::Completed(completion_date),
};
Item::Task(Task::new_with_parameters(name, uid, item_url, completion_status, sync_status, creation_date, last_modified, ical_prod_id, extra_parameters))
},
let completion_percent = completion_percent.unwrap_or(match completed {
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?
if reader.next().map(|r| r.is_ok()) == Some(true) {
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)
}
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> {
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") )
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| parse_date(dt))
}
fn parse_date_time_from_property(value: &Option<String>) -> Option<DateTime<Utc>> {
value.as_ref()
.and_then(|s| {
parse_date_time(s)
value.as_ref().and_then(|s| {
parse_date_time(s)
.map_err(|err| {
log::warn!("Invalid timestamp: {}", s);
log::warn!("Invalid timestamp: '{}'", s);
err
})
.ok()
})
})
}
fn extract_ical_prod_id(item: &IcalCalendar) -> Option<&str> {
for prop in &item.properties {
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
}
enum CurrentType<'a> {
Event(&'a IcalEvent),
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());
}
#[cfg(test)]
mod test {
const EXAMPLE_ICAL: &str = r#"BEGIN:VCALENDAR
@ -195,7 +315,7 @@ END:VTODO
END:VCALENDAR
"#;
const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR
const EXAMPLE_ICAL_COMPLETED: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud Tasks v0.13.6
BEGIN:VTODO
@ -211,7 +331,7 @@ END:VTODO
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
PRODID:-//Nextcloud Tasks v0.13.6
BEGIN:VTODO
@ -261,11 +381,17 @@ END:VCALENDAR
assert_eq!(task.name(), "Do not forget to do this");
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.completion_status(), &CompletionStatus::Uncompleted);
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]
@ -274,11 +400,19 @@ END:VCALENDAR
let sync_status = SyncStatus::Synced(version_tag);
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();
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]
@ -287,7 +421,12 @@ END:VCALENDAR
let sync_status = SyncStatus::Synced(version_tag);
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();
assert_eq!(task.completed(), true);

99
src/task.rs

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

46
src/utils/mod.rs

@ -1,17 +1,17 @@
//! Some utility functions
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::hash::Hash;
use std::io::{stdin, stdout, Read, Write};
use std::sync::{Arc, Mutex};
use minidom::Element;
use url::Url;
use crate::item::SyncStatus;
use crate::traits::CompleteCalendar;
use crate::traits::DavCalendar;
use crate::Item;
use crate::item::SyncStatus;
/// 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> {
@ -49,14 +49,10 @@ pub fn find_elem<S: AsRef<str>>(root: &Element, searched_name: S) -> Option<&Ele
None
}
pub fn print_xml(element: &Element) {
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 _ = writer.write(&[0x0a]);
}
@ -74,7 +70,7 @@ where
for (_, item) in map {
print_task(item);
}
},
}
}
}
}
@ -92,7 +88,7 @@ where
for (url, version_tag) in map {
println!(" * {} (version {:?})", url, version_tag);
}
},
}
}
}
}
@ -100,20 +96,42 @@ where
pub fn print_task(item: &Item) {
match item {
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() {
SyncStatus::NotSynced => ".",
SyncStatus::Synced(_) => "=",
SyncStatus::LocallyModified(_) => "~",
SyncStatus::LocallyDeleted(_) => "x",
SyncStatus::LocallyDeleted(_) => "x",
};
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,
}
}
/// 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
where
@ -140,7 +158,6 @@ where
result
}
/// Wait for the user to press enter
pub fn pause() {
let mut stdout = stdout();
@ -149,7 +166,6 @@ pub fn pause() {
stdin().read_exact(&mut [0]).unwrap();
}
/// Generate a random URL with a given prefix
pub fn random_url(parent_calendar: &Url) -> Url {
let random = uuid::Uuid::new_v4().to_hyphenated().to_string();

Loading…
Cancel
Save