kitchen_fridge/
task.rs

1//! To-do tasks (iCal `VTODO` item)
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5use chrono::{DateTime, Utc};
6use ical::property::Property;
7use url::Url;
8
9use crate::item::SyncStatus;
10use crate::utils::random_url;
11
12/// RFC5545 defines the completion as several optional fields, yet some combinations make no sense.
13/// This enum provides an API that forbids such impossible combinations.
14///
15/// * `COMPLETED` is an optional timestamp that tells whether this task is completed
16/// * `STATUS` is an optional field, that can be set to `NEEDS-ACTION`, `COMPLETED`, or others.
17/// 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
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
19pub enum CompletionStatus {
20    Completed(Option<DateTime<Utc>>),
21    Uncompleted,
22}
23impl CompletionStatus {
24    pub fn is_completed(&self) -> bool {
25        match self {
26            CompletionStatus::Completed(_) => true,
27            _ => false,
28        }
29    }
30}
31
32/// A to-do task
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub struct Task {
35    /// The task URL
36    url: Url,
37
38    /// Persistent, globally unique identifier for the calendar component
39    /// The [RFC](https://tools.ietf.org/html/rfc5545#page-117) recommends concatenating a timestamp with the server's domain name.
40    /// UUID are even better so we'll generate them, but we have to support tasks from the server, that may have any arbitrary strings here.
41    uid: String,
42
43    /// The sync status of this item
44    sync_status: SyncStatus,
45    /// The time this item was created.
46    /// 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
47    creation_date: Option<DateTime<Utc>>,
48    /// The last time this item was modified
49    last_modified: DateTime<Utc>,
50    /// The completion status of this task
51    completion_status: CompletionStatus,
52
53    /// The display name of the task
54    name: String,
55
56
57    /// The PRODID, as defined in iCal files
58    ical_prod_id: String,
59
60    /// Extra parameters that have not been parsed from the iCal file (because they're not supported (yet) by this crate).
61    /// They are needed to serialize this item into an equivalent iCal file
62    extra_parameters: Vec<Property>,
63}
64
65
66impl Task {
67    /// Create a brand new Task that is not on a server yet.
68    /// This will pick a new (random) task ID.
69    pub fn new(name: String, completed: bool, parent_calendar_url: &Url) -> Self {
70        let new_url = random_url(parent_calendar_url);
71        let new_sync_status = SyncStatus::NotSynced;
72        let new_uid = Uuid::new_v4().to_hyphenated().to_string();
73        let new_creation_date = Some(Utc::now());
74        let new_last_modified = Utc::now();
75        let new_completion_status = if completed {
76                CompletionStatus::Completed(Some(Utc::now()))
77            } else { CompletionStatus::Uncompleted };
78        let ical_prod_id = crate::ical::default_prod_id();
79        let extra_parameters = Vec::new();
80        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)
81    }
82
83    /// Create a new Task instance, that may be synced on the server already
84    pub fn new_with_parameters(name: String, uid: String, new_url: Url,
85                               completion_status: CompletionStatus,
86                               sync_status: SyncStatus, creation_date: Option<DateTime<Utc>>, last_modified: DateTime<Utc>,
87                               ical_prod_id: String, extra_parameters: Vec<Property>,
88                            ) -> Self
89    {
90        Self {
91            url: new_url,
92            uid,
93            name,
94            completion_status,
95            sync_status,
96            creation_date,
97            last_modified,
98            ical_prod_id,
99            extra_parameters,
100        }
101    }
102
103    pub fn url(&self) -> &Url       { &self.url         }
104    pub fn uid(&self) -> &str       { &self.uid         }
105    pub fn name(&self) -> &str      { &self.name        }
106    pub fn completed(&self) -> bool { self.completion_status.is_completed() }
107    pub fn ical_prod_id(&self) -> &str            { &self.ical_prod_id }
108    pub fn sync_status(&self) -> &SyncStatus      { &self.sync_status  }
109    pub fn last_modified(&self) -> &DateTime<Utc> { &self.last_modified }
110    pub fn creation_date(&self) -> Option<&DateTime<Utc>>   { self.creation_date.as_ref() }
111    pub fn completion_status(&self) -> &CompletionStatus    { &self.completion_status }
112    pub fn extra_parameters(&self) -> &[Property]           { &self.extra_parameters }
113
114    #[cfg(any(test, feature = "integration_tests"))]
115    pub fn has_same_observable_content_as(&self, other: &Task) -> bool {
116           self.url == other.url
117        && self.uid == other.uid
118        && self.name == other.name
119        // sync status must be the same variant, but we ignore its embedded version tag
120        && std::mem::discriminant(&self.sync_status) == std::mem::discriminant(&other.sync_status)
121        // completion status must be the same variant, but we ignore its embedded completion date (they are not totally mocked in integration tests)
122        && std::mem::discriminant(&self.completion_status) == std::mem::discriminant(&other.completion_status)
123        // last modified dates are ignored (they are not totally mocked in integration tests)
124    }
125
126    pub fn set_sync_status(&mut self, new_status: SyncStatus) {
127        self.sync_status = new_status;
128    }
129
130    fn update_sync_status(&mut self) {
131        match &self.sync_status {
132            SyncStatus::NotSynced => return,
133            SyncStatus::LocallyModified(_) => return,
134            SyncStatus::Synced(prev_vt) => {
135                self.sync_status = SyncStatus::LocallyModified(prev_vt.clone());
136            }
137            SyncStatus::LocallyDeleted(_) => {
138                log::warn!("Trying to update an item that has previously been deleted. These changes will probably be ignored at next sync.");
139                return;
140            },
141        }
142    }
143
144    fn update_last_modified(&mut self) {
145        self.last_modified = Utc::now();
146    }
147
148
149    /// Rename a task.
150    /// This updates its "last modified" field
151    pub fn set_name(&mut self, new_name: String) {
152        self.update_sync_status();
153        self.update_last_modified();
154        self.name = new_name;
155    }
156    #[cfg(feature = "local_calendar_mocks_remote_calendars")]
157    /// Rename a task, but forces a "master" SyncStatus, just like CalDAV servers are always "masters"
158    pub fn mock_remote_calendar_set_name(&mut self, new_name: String) {
159        self.sync_status = SyncStatus::random_synced();
160        self.update_last_modified();
161        self.name = new_name;
162    }
163
164    /// Set the completion status
165    pub fn set_completion_status(&mut self, new_completion_status: CompletionStatus) {
166        self.update_sync_status();
167        self.update_last_modified();
168        self.completion_status = new_completion_status;
169    }
170    #[cfg(feature = "local_calendar_mocks_remote_calendars")]
171    /// Set the completion status, but forces a "master" SyncStatus, just like CalDAV servers are always "masters"
172    pub fn mock_remote_calendar_set_completion_status(&mut self, new_completion_status: CompletionStatus) {
173        self.sync_status = SyncStatus::random_synced();
174        self.completion_status = new_completion_status;
175    }
176}