taud/
month_tasks.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20    collections::HashMap,
21    fs, io,
22    path::{Path, PathBuf},
23};
24
25use chrono::{TimeZone, Utc};
26use log::debug;
27use tinyjson::JsonValue;
28
29use darkfi::util::{
30    file::{load_json_file, save_json_file},
31    time::Timestamp,
32};
33
34use crate::{
35    error::{TaudError, TaudResult},
36    task_info::TaskInfo,
37};
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct MonthTasks {
41    created_at: Timestamp,
42    active_tks: Vec<String>,
43    deactive_tks: Vec<String>,
44}
45
46impl From<MonthTasks> for JsonValue {
47    fn from(mt: MonthTasks) -> JsonValue {
48        let active_tks: Vec<JsonValue> =
49            mt.active_tks.iter().map(|x| JsonValue::String(x.clone())).collect();
50
51        let deactive_tks: Vec<JsonValue> =
52            mt.deactive_tks.iter().map(|x| JsonValue::String(x.clone())).collect();
53
54        JsonValue::Object(HashMap::from([
55            ("created_at".to_string(), JsonValue::String(mt.created_at.inner().to_string())),
56            ("active_tks".to_string(), JsonValue::Array(active_tks)),
57            ("deactive_tks".to_string(), JsonValue::Array(deactive_tks)),
58        ]))
59    }
60}
61
62impl From<JsonValue> for MonthTasks {
63    fn from(value: JsonValue) -> MonthTasks {
64        let created_at = {
65            let u64_str = value["created_at"].get::<String>().unwrap();
66            Timestamp::from_u64(u64_str.parse::<u64>().unwrap())
67        };
68
69        let active_tks: Vec<String> = value["active_tks"]
70            .get::<Vec<JsonValue>>()
71            .unwrap()
72            .iter()
73            .map(|x| x.get::<String>().unwrap().clone())
74            .collect();
75
76        let deactive_tks: Vec<String> = value["deactive_tks"]
77            .get::<Vec<JsonValue>>()
78            .unwrap()
79            .iter()
80            .map(|x| x.get::<String>().unwrap().clone())
81            .collect();
82
83        MonthTasks { created_at, active_tks, deactive_tks }
84    }
85}
86
87impl MonthTasks {
88    pub fn new(active_tks: &[String], deactive_tks: &[String]) -> Self {
89        Self {
90            created_at: Timestamp::current_time(),
91            active_tks: active_tks.to_owned(),
92            deactive_tks: deactive_tks.to_owned(),
93        }
94    }
95
96    pub fn add(&mut self, ref_id: &str) {
97        debug!(target: "tau", "MonthTasks::add()");
98        if !self.active_tks.contains(&ref_id.into()) {
99            self.active_tks.push(ref_id.into());
100        }
101    }
102
103    pub fn objects(&self, dataset_path: &Path) -> TaudResult<Vec<TaskInfo>> {
104        debug!(target: "tau", "MonthTasks::objects()");
105        let mut tks: Vec<TaskInfo> = vec![];
106
107        for ref_id in self.active_tks.iter() {
108            tks.push(TaskInfo::load(ref_id, dataset_path)?);
109        }
110
111        for ref_id in self.deactive_tks.iter() {
112            tks.push(TaskInfo::load(ref_id, dataset_path)?);
113        }
114
115        Ok(tks)
116    }
117
118    pub fn remove(&mut self, ref_id: &str) {
119        debug!(target: "tau", "MonthTasks::remove()");
120        if self.active_tks.contains(&ref_id.to_string()) {
121            if let Some(index) = self.active_tks.iter().position(|t| *t == ref_id) {
122                self.deactive_tks.push(self.active_tks.remove(index));
123            }
124        } else {
125            self.deactive_tks.push(ref_id.to_owned());
126        }
127    }
128
129    pub fn set_date(&mut self, date: &Timestamp) {
130        debug!(target: "tau", "MonthTasks::set_date()");
131        self.created_at = *date;
132    }
133
134    fn get_path(date: &Timestamp, dataset_path: &Path) -> PathBuf {
135        debug!(target: "tau", "MonthTasks::get_path()");
136        dataset_path.join("month").join(
137            Utc.timestamp_opt(date.inner().try_into().unwrap(), 0)
138                .unwrap()
139                .format("%m%y")
140                .to_string(),
141        )
142    }
143
144    pub fn save(&self, dataset_path: &Path) -> TaudResult<()> {
145        debug!(target: "tau", "MonthTasks::save()");
146        let mt: JsonValue = self.clone().into();
147        save_json_file(&Self::get_path(&self.created_at, dataset_path), &mt, true)
148            .map_err(TaudError::Darkfi)
149    }
150
151    fn get_all(dataset_path: &Path) -> io::Result<Vec<PathBuf>> {
152        debug!(target: "tau", "MonthTasks::get_all()");
153
154        let mut entries = fs::read_dir(dataset_path.join("month"))?
155            .map(|res| res.map(|e| e.path()))
156            .collect::<Result<Vec<_>, io::Error>>()?;
157
158        entries.sort();
159
160        Ok(entries)
161    }
162
163    fn create(date: &Timestamp, dataset_path: &Path) -> TaudResult<Self> {
164        debug!(target: "tau", "MonthTasks::create()");
165
166        let mut mt = Self::new(&[], &[]);
167        mt.set_date(date);
168        mt.save(dataset_path)?;
169        Ok(mt)
170    }
171
172    pub fn load_or_create(date: Option<&Timestamp>, dataset_path: &Path) -> TaudResult<Self> {
173        debug!(target: "tau", "MonthTasks::load_or_create()");
174
175        // if a date is given we load that date's month tasks
176        // if not, we load tasks from all months
177        match date {
178            Some(date) => match load_json_file(&Self::get_path(date, dataset_path)) {
179                Ok(mt) => Ok(mt.into()),
180                Err(_) => Self::create(date, dataset_path),
181            },
182            None => {
183                let path_all = Self::get_all(dataset_path).unwrap_or_default();
184
185                let mut loaded_mt = Self::new(&[], &[]);
186
187                for path in path_all {
188                    let mt = load_json_file(&path)?;
189                    let mt: MonthTasks = mt.into();
190                    loaded_mt.created_at = mt.created_at;
191                    for tks in mt.active_tks {
192                        if !loaded_mt.active_tks.contains(&tks) {
193                            loaded_mt.active_tks.push(tks)
194                        }
195                    }
196                    for dtks in mt.deactive_tks {
197                        if !loaded_mt.deactive_tks.contains(&dtks) {
198                            loaded_mt.deactive_tks.push(dtks)
199                        }
200                    }
201                }
202                Ok(loaded_mt)
203            }
204        }
205    }
206
207    pub fn load_current_tasks(
208        dataset_path: &Path,
209        ws: String,
210        all: bool,
211    ) -> TaudResult<Vec<TaskInfo>> {
212        let mt = Self::load_or_create(None, dataset_path)?;
213
214        if all {
215            Ok(mt.objects(dataset_path)?.into_iter().filter(|t| t.workspace == ws).collect())
216        } else {
217            Ok(mt
218                .objects(dataset_path)?
219                .into_iter()
220                .filter(|t| t.get_state() != "stop" && t.workspace == ws)
221                .collect())
222        }
223    }
224
225    pub fn load_stop_tasks(
226        dataset_path: &Path,
227        ws: String,
228        date: Option<&Timestamp>,
229    ) -> TaudResult<Vec<TaskInfo>> {
230        let mt = Self::load_or_create(date, dataset_path)?;
231        Ok(mt
232            .objects(dataset_path)?
233            .into_iter()
234            .filter(|t| t.get_state() == "stop" && t.workspace == ws)
235            .collect())
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use std::fs::{create_dir_all, remove_dir_all};
242
243    use super::*;
244    use darkfi::Result;
245
246    const TEST_DATA_PATH: &str = "/tmp/test_tau_data";
247
248    fn get_path() -> Result<PathBuf> {
249        remove_dir_all(TEST_DATA_PATH).ok();
250
251        let path = PathBuf::from(TEST_DATA_PATH);
252
253        // mkdir dataset_path if not exists
254        create_dir_all(path.join("month"))?;
255        create_dir_all(path.join("task"))?;
256        Ok(path)
257    }
258
259    #[test]
260    fn load_and_save_tasks() -> TaudResult<()> {
261        let dataset_path = get_path()?;
262
263        // load and save TaskInfo
264        ///////////////////////
265
266        let mut task = TaskInfo::new(
267            "darkfi".to_string(),
268            "test_title",
269            "test_desc",
270            "NICKNAME",
271            None,
272            Some(0.0),
273            Timestamp::current_time(),
274        )?;
275
276        task.save(&dataset_path)?;
277
278        let t_load = TaskInfo::load(&task.ref_id, &dataset_path)?;
279
280        assert_eq!(task, t_load);
281
282        task.set_title("test_title_2");
283
284        task.save(&dataset_path)?;
285
286        let t_load = TaskInfo::load(&task.ref_id, &dataset_path)?;
287
288        assert_eq!(task, t_load);
289
290        // load and save MonthTasks
291        ///////////////////////
292
293        let task_tks = vec![];
294
295        let mut mt = MonthTasks::new(&task_tks, &[]);
296
297        mt.save(&dataset_path)?;
298
299        let mt_load = MonthTasks::load_or_create(Some(&Timestamp::current_time()), &dataset_path)?;
300
301        assert_eq!(mt, mt_load);
302
303        mt.add(&task.ref_id);
304
305        mt.save(&dataset_path)?;
306
307        let mt_load = MonthTasks::load_or_create(Some(&Timestamp::current_time()), &dataset_path)?;
308
309        assert_eq!(mt, mt_load);
310
311        // activate task
312        ///////////////////////
313
314        let task = TaskInfo::new(
315            "darkfi".to_string(),
316            "test_title_3",
317            "test_desc",
318            "NICKNAME",
319            None,
320            Some(0.0),
321            Timestamp::current_time(),
322        )?;
323
324        task.save(&dataset_path)?;
325
326        let mt_load = MonthTasks::load_or_create(Some(&Timestamp::current_time()), &dataset_path)?;
327
328        assert!(mt_load.active_tks.contains(&task.ref_id));
329
330        remove_dir_all(TEST_DATA_PATH).ok();
331
332        Ok(())
333    }
334}