use crate::argparse::Filter; use crate::invocation::filtered_tasks; use crate::settings::{Column, Property, Report, Settings, SortBy}; use crate::table; use anyhow::anyhow; use prettytable::{Row, Table}; use std::cmp::Ordering; use taskchampion::{Replica, Task, WorkingSet}; use termcolor::WriteColor; /// Sort tasks for the given report. fn sort_tasks(tasks: &mut Vec, report: &Report, working_set: &WorkingSet) { tasks.sort_by(|a, b| { for s in &report.sort { let ord = match s.sort_by { SortBy::Id => { let a_uuid = a.get_uuid(); let b_uuid = b.get_uuid(); let a_id = working_set.by_uuid(a_uuid); let b_id = working_set.by_uuid(b_uuid); match (a_id, b_id) { (Some(a_id), Some(b_id)) => a_id.cmp(&b_id), (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, (None, None) => a_uuid.cmp(&b_uuid), } } SortBy::Uuid => a.get_uuid().cmp(&b.get_uuid()), SortBy::Description => a.get_description().cmp(b.get_description()), SortBy::Wait => a.get_wait().cmp(&b.get_wait()), }; // If this sort property is equal, go on to the next.. if ord == Ordering::Equal { continue; } // Reverse order if not ascending if s.ascending { return ord; } else { return ord.reverse(); } } Ordering::Equal }); } /// Generate the string representation for the given task and column. fn task_column(task: &Task, column: &Column, working_set: &WorkingSet) -> String { match column.property { Property::Id => { let uuid = task.get_uuid(); let mut id = uuid.to_string(); if let Some(i) = working_set.by_uuid(uuid) { id = i.to_string(); } id } Property::Uuid => { let uuid = task.get_uuid(); uuid.to_string() } Property::Active => match task.is_active() { true => "*".to_owned(), false => "".to_owned(), }, Property::Description => task.get_description().to_owned(), Property::Tags => { let mut tags = task .get_tags() .map(|t| format!("+{}", t)) .collect::>(); tags.sort(); tags.join(" ") } Property::Wait => { if task.is_waiting() { task.get_wait().unwrap().format("%Y-%m-%d").to_string() } else { "".to_owned() } } } } pub(super) fn display_report( w: &mut W, replica: &mut Replica, settings: &Settings, report_name: String, filter: Filter, ) -> Result<(), crate::Error> { let mut t = Table::new(); let working_set = replica.working_set()?; // Get the report from settings let mut report = settings .reports .get(&report_name) .ok_or_else(|| anyhow!("report `{}` not defined", report_name))? .clone(); // include any user-supplied filter conditions report.filter = report.filter.intersect(filter); // Get the tasks from the filter let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect(); // ..sort them as desired sort_tasks(&mut tasks, &report, &working_set); // ..set up the column titles t.set_format(table::format()); t.set_titles(report.columns.iter().map(|col| col.label.clone()).into()); // ..insert the data for task in &tasks { let row: Row = report .columns .iter() .map(|col| task_column(task, col, &working_set)) .collect::(); t.add_row(row); } // ..and display it t.print(w)?; Ok(()) } #[cfg(test)] mod test { use super::*; use crate::invocation::test::*; use crate::settings::Sort; use chrono::prelude::*; use pretty_assertions::assert_eq; use std::convert::TryInto; use taskchampion::{Status, Uuid}; fn create_tasks(replica: &mut Replica) -> [Uuid; 3] { let t1 = replica.new_task(Status::Pending, s!("A")).unwrap(); let t2 = replica.new_task(Status::Pending, s!("B")).unwrap(); let t3 = replica.new_task(Status::Pending, s!("C")).unwrap(); // t2 is comleted and not in the working set let mut t2 = t2.into_mut(replica); t2.set_status(Status::Completed).unwrap(); let t2 = t2.into_immut(); replica.rebuild_working_set(true).unwrap(); [t1.get_uuid(), t2.get_uuid(), t3.get_uuid()] } #[test] fn sorting_by_descr() { let mut replica = test_replica(); create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); let mut report = Report { sort: vec![Sort { ascending: true, sort_by: SortBy::Description, }], ..Default::default() }; // ascending let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); sort_tasks(&mut tasks, &report, &working_set); let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); assert_eq!(descriptions, vec!["A", "B", "C"]); // ascending report.sort[0].ascending = false; let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); sort_tasks(&mut tasks, &report, &working_set); let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); assert_eq!(descriptions, vec!["C", "B", "A"]); } #[test] fn sorting_by_id() { let mut replica = test_replica(); create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); let mut report = Report { sort: vec![Sort { ascending: true, sort_by: SortBy::Id, }], ..Default::default() }; // ascending let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); sort_tasks(&mut tasks, &report, &working_set); let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); assert_eq!(descriptions, vec!["A", "C", "B"]); // ascending report.sort[0].ascending = false; let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); sort_tasks(&mut tasks, &report, &working_set); let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); assert_eq!(descriptions, vec!["B", "C", "A"]); } #[test] fn sorting_by_uuid() { let mut replica = test_replica(); let uuids = create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); let report = Report { sort: vec![Sort { ascending: true, sort_by: SortBy::Uuid, }], ..Default::default() }; let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); sort_tasks(&mut tasks, &report, &working_set); let got_uuids: Vec<_> = tasks.iter().map(|t| t.get_uuid()).collect(); let mut exp_uuids = uuids.to_vec(); exp_uuids.sort(); assert_eq!(got_uuids, exp_uuids); } #[test] fn sorting_by_wait() { let mut replica = test_replica(); let uuids = create_tasks(&mut replica); replica .get_task(uuids[0]) .unwrap() .unwrap() .into_mut(&mut replica) .set_wait(Some(Utc::now() + chrono::Duration::days(2))) .unwrap(); replica .get_task(uuids[1]) .unwrap() .unwrap() .into_mut(&mut replica) .set_wait(Some(Utc::now() + chrono::Duration::days(3))) .unwrap(); let working_set = replica.working_set().unwrap(); let report = Report { sort: vec![Sort { ascending: true, sort_by: SortBy::Wait, }], ..Default::default() }; let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); sort_tasks(&mut tasks, &report, &working_set); let got_uuids: Vec<_> = tasks.iter().map(|t| t.get_uuid()).collect(); let exp_uuids = vec![ uuids[2], // no wait uuids[0], // wait:2d uuids[1], // wait:3d ]; assert_eq!(got_uuids, exp_uuids); } #[test] fn sorting_by_multiple() { let mut replica = test_replica(); create_tasks(&mut replica); // make a second task named A with a larger ID than the first let t = replica.new_task(Status::Pending, s!("A")).unwrap(); t.into_mut(&mut replica) .add_tag(&("second".try_into().unwrap())) .unwrap(); let working_set = replica.working_set().unwrap(); let report = Report { sort: vec![ Sort { ascending: false, sort_by: SortBy::Description, }, Sort { ascending: true, sort_by: SortBy::Id, }, ], ..Default::default() }; let mut tasks: Vec<_> = replica.all_tasks().unwrap().values().cloned().collect(); sort_tasks(&mut tasks, &report, &working_set); let descriptions: Vec<_> = tasks.iter().map(|t| t.get_description()).collect(); assert_eq!(descriptions, vec!["C", "B", "A", "A"]); assert!(tasks[3].has_tag(&("second".try_into().unwrap()))); } #[test] fn task_column_id() { let mut replica = test_replica(); let uuids = create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); let task = replica.get_task(uuids[0]).unwrap().unwrap(); let column = Column { label: s!(""), property: Property::Id, }; assert_eq!(task_column(&task, &column, &working_set), s!("1")); // get the task that's not in the working set, which should show // a uuid for its id column let task = replica.get_task(uuids[1]).unwrap().unwrap(); assert_eq!( task_column(&task, &column, &working_set), uuids[1].to_string() ); } #[test] fn task_column_uuid() { let mut replica = test_replica(); let uuids = create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); let task = replica.get_task(uuids[0]).unwrap().unwrap(); let column = Column { label: s!(""), property: Property::Uuid, }; assert_eq!( task_column(&task, &column, &working_set), task.get_uuid().to_string() ); } #[test] fn task_column_active() { let mut replica = test_replica(); let uuids = create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); // make task A active replica .get_task(uuids[0]) .unwrap() .unwrap() .into_mut(&mut replica) .start() .unwrap(); let column = Column { label: s!(""), property: Property::Active, }; let task = replica.get_task(uuids[0]).unwrap().unwrap(); assert_eq!(task_column(&task, &column, &working_set), s!("*")); let task = replica.get_task(uuids[2]).unwrap().unwrap(); assert_eq!(task_column(&task, &column, &working_set), s!("")); } #[test] fn task_column_description() { let mut replica = test_replica(); let uuids = create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); let task = replica.get_task(uuids[2]).unwrap().unwrap(); let column = Column { label: s!(""), property: Property::Description, }; assert_eq!(task_column(&task, &column, &working_set), s!("C")); } #[test] fn task_column_tags() { let mut replica = test_replica(); let uuids = create_tasks(&mut replica); let working_set = replica.working_set().unwrap(); // add some tags to task A let mut t1 = replica .get_task(uuids[0]) .unwrap() .unwrap() .into_mut(&mut replica); t1.add_tag(&("foo".try_into().unwrap())).unwrap(); t1.add_tag(&("bar".try_into().unwrap())).unwrap(); let column = Column { label: s!(""), property: Property::Tags, }; let task = replica.get_task(uuids[0]).unwrap().unwrap(); assert_eq!( task_column(&task, &column, &working_set), s!("+PENDING +bar +foo") ); let task = replica.get_task(uuids[2]).unwrap().unwrap(); assert_eq!(task_column(&task, &column, &working_set), s!("+PENDING")); } }