From d040ca6eec4436717e44a4ef5baf79a173cd5250 Mon Sep 17 00:00:00 2001 From: Alan Vardy Date: Mon, 16 Oct 2023 18:39:29 -0700 Subject: [PATCH] Add filter schedule and prioritize --- CHANGELOG.md | 2 + README.md | 1 + src/filters.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 28 ++++++++++ src/projects.rs | 2 +- src/test.rs | 36 ++++++++++++- 6 files changed, 196 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a93fb07..cfc092ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Remove `scheduled` flag from `task list`, the `filter` flag covers this use case now. Use `today & !no time`. - Add `filter` flag to `task edit` and `task next` - Add `filter label` +- Add `filter schedule` +- Add `filter prioritize` ## 2023-10-09 v0.5.3 diff --git a/README.md b/README.md index d87c41cf..de11e4c6 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Usage: tod [OPTIONS] [COMMAND] Commands: task project + filter version help Print this message or the help of the given subcommand(s) diff --git a/src/filters.rs b/src/filters.rs index e695f27f..aaf4b7a9 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,7 +1,7 @@ use crate::{ color, config::Config, - input, + input::{self, DateTimeInput}, tasks::{self, FormatType, Task}, todoist, }; @@ -122,6 +122,61 @@ fn handle_task(config: &Config, task: Task) -> Option> { } } +/// Prioritize all unprioritized tasks in a project +pub fn prioritize_tasks(config: &Config, filter: &String) -> Result { + let tasks = todoist::tasks_for_filter(config, filter)?; + + if tasks.is_empty() { + Ok(color::green_string(&format!( + "No tasks to prioritize in '{filter}'" + ))) + } else { + for task in tasks.iter() { + tasks::set_priority(config, task.to_owned())?; + } + Ok(color::green_string(&format!( + "Successfully prioritized '{filter}'" + ))) + } +} + +/// Put dates on all tasks without dates +pub fn schedule(config: &Config, filter: &String) -> Result { + let tasks = todoist::tasks_for_filter(config, filter)?; + + if tasks.is_empty() { + Ok(color::green_string(&format!( + "No tasks to schedule in '{filter}'" + ))) + } else { + for task in tasks.iter() { + println!("{}", task.fmt(config, FormatType::Single)); + let datetime_input = input::datetime( + config.mock_select, + config.mock_string.clone(), + config.natural_language_only, + )?; + match datetime_input { + input::DateTimeInput::Complete => { + let config = config.set_next_id(&task.id); + todoist::complete_task(&config)? + } + DateTimeInput::Skip => "Skipped".to_string(), + + input::DateTimeInput::Text(due_string) => { + todoist::update_task_due(config, task.to_owned(), due_string)? + } + input::DateTimeInput::None => { + todoist::update_task_due(config, task.to_owned(), "No Date".to_string())? + } + }; + } + Ok(color::green_string(&format!( + "Successfully scheduled tasks in '{filter}'" + ))) + } +} + #[cfg(test)] mod tests { use super::*; @@ -138,7 +193,7 @@ mod tests { .mock("GET", "/rest/v2/tasks/?filter=today") .with_status(200) .with_header("content-type", "application/json") - .with_body(test::responses::rest_tasks()) + .with_body(test::responses::get_tasks()) .create(); let config = test::fixtures::config().mock_url(server.url()); @@ -167,7 +222,7 @@ mod tests { .mock("GET", "/rest/v2/tasks/?filter=today") .with_status(200) .with_header("content-type", "application/json") - .with_body(test::responses::rest_tasks()) + .with_body(test::responses::get_tasks()) .create(); let config = test::fixtures::config() @@ -188,7 +243,7 @@ mod tests { .mock("GET", "/rest/v2/tasks/?filter=today") .with_status(200) .with_header("content-type", "application/json") - .with_body(test::responses::rest_tasks()) + .with_body(test::responses::get_tasks()) .create(); let config = test::fixtures::config().mock_url(server.url()); @@ -219,14 +274,14 @@ mod tests { .mock("GET", "/rest/v2/tasks/?filter=today") .with_status(200) .with_header("content-type", "application/json") - .with_body(test::responses::rest_tasks()) + .with_body(test::responses::get_tasks()) .create(); let mock2 = server .mock("POST", "/rest/v2/tasks/999999") .with_status(200) .with_header("content-type", "application/json") - .with_body(test::responses::rest_tasks()) + .with_body(test::responses::get_tasks()) .create(); let config = test::fixtures::config().mock_url(server.url()); @@ -261,7 +316,7 @@ mod tests { .mock("GET", "/rest/v2/tasks/?filter=today") .with_status(200) .with_header("content-type", "application/json") - .with_body(test::responses::rest_tasks()) + .with_body(test::responses::get_tasks()) .create(); let mock2 = server @@ -286,4 +341,72 @@ mod tests { mock.assert(); mock2.assert(); } + + #[test] + fn test_schedule() { + let mut server = mockito::Server::new(); + let mock = server + .mock("GET", "/rest/v2/tasks/?filter=today") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(test::responses::get_unscheduled_tasks()) + .create(); + + let mock2 = server + .mock("POST", "/rest/v2/tasks/999999") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(test::responses::task()) + .create(); + + let config = test::fixtures::config() + .mock_url(server.url()) + .mock_select(1) + .mock_string("tod"); + + let filter = String::from("today"); + let result = schedule(&config, &filter); + assert_eq!( + result, + Ok("Successfully scheduled tasks in 'today'".to_string()) + ); + + let config = config.mock_select(2); + + let filter = String::from("today"); + let result = schedule(&config, &filter); + assert_eq!( + result, + Ok("Successfully scheduled tasks in 'today'".to_string()) + ); + + mock.expect(2); + mock2.expect(2); + } + #[test] + fn test_prioritize_tasks() { + let mut server = mockito::Server::new(); + let mock = server + .mock("GET", "/rest/v2/tasks/?filter=today") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(test::responses::get_tasks()) + .create(); + let mock2 = server + .mock("POST", "/rest/v2/tasks/999999") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(test::responses::get_tasks()) + .create(); + + let config = test::fixtures::config() + .mock_url(server.url()) + .mock_select(1); + + let filter = String::from("today"); + let result = prioritize_tasks(&config, &filter); + assert_eq!(result, Ok(String::from("Successfully prioritized 'today'"))); + mock.assert(); + mock2.assert(); + } } diff --git a/src/main.rs b/src/main.rs index 58f73e1d..9ef5d887 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,8 @@ fn main() { Some(("filter", filter_matches)) => match filter_matches.subcommand() { Some(("label", m)) => filter_label(m), Some(("process", m)) => filter_process(m), + Some(("prioritize", m)) => filter_prioritize(m), + Some(("schedule", m)) => filter_schedule(m), _ => unreachable!(), }, Some(("version", version_matches)) => match version_matches.subcommand() { @@ -213,6 +215,14 @@ fn cmd() -> Command { .arg(flag_arg("verbose", 'v', "Display additional debug info while processing")) .arg(filter_arg()) .arg(config_arg()), + Command::new("prioritize").about("Give every task a priority") + .arg(flag_arg("verbose", 'v', "Display additional debug info while processing")) + .arg(filter_arg()) + .arg(config_arg()), + Command::new("schedule").about("Assign dates to all tasks individually") + .arg(flag_arg("verbose", 'v', "Display additional debug info while processing")) + .arg(filter_arg()) + .arg(config_arg()), ] ), Command::new("version") @@ -436,6 +446,24 @@ fn filter_process(matches: &ArgMatches) -> Result { _ => unreachable!(), } } + +#[cfg(not(tarpaulin_include))] +fn filter_prioritize(matches: &ArgMatches) -> Result { + let config = fetch_config(matches)?; + match fetch_project(matches, &config)? { + Flag::Filter(filter) => filters::prioritize_tasks(&config, &filter), + _ => unreachable!(), + } +} + +#[cfg(not(tarpaulin_include))] +fn filter_schedule(matches: &ArgMatches) -> Result { + let config = fetch_config(matches)?; + match fetch_project(matches, &config)? { + Flag::Filter(filter) => filters::schedule(&config, &filter), + _ => unreachable!(), + } +} // --- VERSION --- #[cfg(not(tarpaulin_include))] diff --git a/src/projects.rs b/src/projects.rs index a4b4fe7b..7b748110 100644 --- a/src/projects.rs +++ b/src/projects.rs @@ -796,7 +796,7 @@ mod tests { .mock("POST", "/sync/v9/projects/get_data") .with_status(200) .with_header("content-type", "application/json") - .with_body(test::responses::unscheduled_tasks()) + .with_body(test::responses::post_unscheduled_tasks()) .create(); let mock2 = server diff --git a/src/test.rs b/src/test.rs index 71e0a9a3..c68a89b8 100644 --- a/src/test.rs +++ b/src/test.rs @@ -155,7 +155,7 @@ pub mod responses { time::today_string(&fixtures::config()) ) } - pub fn rest_tasks() -> String { + pub fn get_tasks() -> String { format!( " [ @@ -193,7 +193,7 @@ pub mod responses { ) } - pub fn unscheduled_tasks() -> String { + pub fn post_unscheduled_tasks() -> String { String::from( "{\ \"items\":\ @@ -226,6 +226,38 @@ pub mod responses { ) } + pub fn get_unscheduled_tasks() -> String { + String::from( + " + [ + {\ + \"added_by_uid\":44444444,\ + \"assigned_by_uid\":null,\ + \"checked\":false,\ + \"child_order\":-5,\ + \"collapsed\":false,\ + \"content\":\"Put out recycling\",\ + \"date_added\":\"2021-06-15T13:01:28Z\",\ + \"date_completed\":null,\ + \"description\":\"\",\ + \"due\":null,\ + \"id\":\"999999\",\ + \"is_deleted\":false,\ + \"labels\":[],\ + \"note_count\":0,\ + \"parent_id\":null,\ + \"priority\":3,\ + \"project_id\":22222222,\ + \"responsible_uid\":null,\ + \"section_id\":333333333,\ + \"sync_id\":null,\ + \"user_id\":111111111\ + } + ] + ", + ) + } + pub fn task() -> String { String::from( "\