A CalDAV client written in Rust
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

158 lines
6.5 KiB

//! To-do tasks (iCal `VTODO` item)
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::item::ItemId;
use crate::item::SyncStatus;
use crate::calendar::CalendarId;
/// RFC5545 defines the completion as several optional fields, yet some combinations make no sense.
/// This enum provides an API that forbids such impossible combinations.
///
/// * `COMPLETED` is an optional timestamp that tells whether this task is completed
/// * `STATUS` is an optional field, that can be set to `NEEDS-ACTION`, `COMPLETED`, or others.
/// Even though having a `COMPLETED` date but a `STATUS:NEEDS-ACTION` is theorically possible, it obviously makes no sense. This API ensures this cannot happen
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum CompletionStatus {
Completed(Option<DateTime<Utc>>),
Uncompleted,
}
impl CompletionStatus {
pub fn is_completed(&self) -> bool {
match self {
CompletionStatus::Completed(_) => true,
_ => false,
}
}
}
/// A to-do task
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Task {
/// The task URL
id: ItemId,
/// Persistent, globally unique identifier for the calendar component
/// The [RFC](https://tools.ietf.org/html/rfc5545#page-117) recommends concatenating a timestamp with the server's domain name, but UUID are even better
uid: String,
/// The sync status of this item
sync_status: SyncStatus,
/// The time this item was created.
/// This is not required by RFC5545. This will be populated in tasks created by this crate, but can be None for tasks coming from a server
creation_date: Option<DateTime<Utc>>,
/// The last time this item was modified
last_modified: DateTime<Utc>,
/// The completion status of this task
completion_status: CompletionStatus,
/// The display name of the task
name: String,
}
impl Task {
/// Create a brand new Task that is not on a server yet.
/// This will pick a new (random) task ID.
pub fn new(name: String, completed: bool, parent_calendar_id: &CalendarId) -> Self {
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();
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 };
Self::new_with_parameters(name, new_uid, new_item_id, new_completion_status, new_sync_status, new_creation_date, new_last_modified)
}
/// Create a new Task instance, that may be synced on the server already
pub fn new_with_parameters(name: String, uid: String, id: ItemId,
completion_status: CompletionStatus,
sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>) -> Self
{
Self {
id,
uid,
name,
completion_status,
sync_status,
creation_date,
last_modified,
}
}
pub fn id(&self) -> &ItemId { &self.id }
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 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 }
#[cfg(any(test, feature = "integration_tests"))]
pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
self.id == other.id
&& self.name == other.name
// sync status must be the same variant, but we ignore its embedded version tag
&& std::mem::discriminant(&self.sync_status) == std::mem::discriminant(&other.sync_status)
// completion status must be the same variant, but we ignore its embedded completion date (they are not totally mocked in integration tests)
&& std::mem::discriminant(&self.completion_status) == std::mem::discriminant(&other.completion_status)
// last modified dates are ignored (they are not totally mocked in integration tests)
}
pub fn set_sync_status(&mut self, new_status: SyncStatus) {
self.sync_status = new_status;
}
fn update_sync_status(&mut self) {
match &self.sync_status {
SyncStatus::NotSynced => return,
SyncStatus::LocallyModified(_) => return,
SyncStatus::Synced(prev_vt) => {
self.sync_status = SyncStatus::LocallyModified(prev_vt.clone());
}
SyncStatus::LocallyDeleted(_) => {
log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync.");
return;
},
}
}
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")]
/// Rename a task, but forces a "master" SyncStatus, just like CalDAV servers are always "masters"
pub fn mock_remote_calendar_set_name(&mut self, new_name: String) {
self.sync_status = SyncStatus::random_synced();
self.update_last_modified();
self.name = new_name;
}
/// Set the completion status
pub fn set_completion_status(&mut self, new_completion_status: CompletionStatus) {
self.update_sync_status();
self.update_last_modified();
self.completion_status = new_completion_status;
}
#[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) {
self.sync_status = SyncStatus::random_synced();
self.completion_status = new_completion_status;
}
}