Merge pull request #313 from djmitche/issue89

Add support for annotations
This commit is contained in:
Dustin J. Mitchell 2021-10-31 09:49:25 -04:00 committed by GitHub
commit acd4aefc17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 173 additions and 11 deletions

View file

@ -48,6 +48,9 @@ pub struct Modification {
/// Remove tags
pub remove_tags: HashSet<Tag>,
/// Add annotation
pub annotate: Option<String>,
}
/// A single argument that is part of a modification, used internally to this module
@ -128,12 +131,12 @@ impl Modification {
pub(super) fn get_usage(u: &mut usage::Usage) {
u.modifications.push(usage::Modification {
syntax: "DESCRIPTION",
summary: "Set description",
summary: "Set description/annotation",
description: "
Set the task description. Multiple arguments are combined into a single
space-separated description. To avoid surprises from shell quoting, prefer
to use a single quoted argument, for example `ta 19 modify \"return library
books\"`",
Set the task description (or the task annotation for `ta annotate`). Multiple
arguments are combined into a single space-separated description. To avoid
surprises from shell quoting, prefer to use a single quoted argument, for example
`ta 19 modify \"return library books\"`",
});
u.modifications.push(usage::Modification {
syntax: "+TAG",

View file

@ -207,6 +207,13 @@ impl Modify {
"start" => modification.active = Some(true),
"stop" => modification.active = Some(false),
"done" => modification.status = Some(Status::Completed),
"annotate" => {
// what would be parsed as a description is, here, used as the annotation
if let DescriptionMod::Set(s) = modification.description {
modification.description = DescriptionMod::None;
modification.annotate = Some(s);
}
}
_ => {}
}
@ -225,6 +232,7 @@ impl Modify {
arg_matching(literal("start")),
arg_matching(literal("stop")),
arg_matching(literal("done")),
arg_matching(literal("annotate")),
)),
Modification::parse,
)),
@ -278,6 +286,13 @@ impl Modify {
Mark all tasks matching the required filter as completed, additionally applying any given
modifications.",
});
u.subcommands.push(usage::Subcommand {
name: "annotate",
syntax: "<filter> annotate [modification]",
summary: "Mark tasks as completed",
description: "
Add an annotation to all tasks matching the required filter.",
});
}
}
@ -652,6 +667,23 @@ mod test {
);
}
#[test]
fn test_annotate() {
let subcommand = Subcommand::Modify {
filter: Filter {
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
},
modification: Modification {
annotate: Some("sent invoice".into()),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "annotate", "sent", "invoice"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_report() {
let subcommand = Subcommand::Report {

View file

@ -39,6 +39,11 @@ pub(crate) fn execute<W: WriteColor>(
if let Some(wait) = task.get_wait() {
t.add_row(row![b->"Wait", wait]);
}
let mut annotations: Vec<_> = task.get_annotations().collect();
annotations.sort();
for ann in annotations {
t.add_row(row![b->"Annotation", format!("{}: {}", ann.entry, ann.description)]);
}
}
t.print(w)?;
}

View file

@ -1,5 +1,6 @@
use crate::argparse::{DescriptionMod, Modification};
use taskchampion::TaskMut;
use chrono::Utc;
use taskchampion::{Annotation, TaskMut};
/// Apply the given modification
pub(super) fn apply_modification(
@ -41,5 +42,12 @@ pub(super) fn apply_modification(
task.set_wait(wait)?;
}
if let Some(ref ann) = modification.annotate {
task.add_annotation(Annotation {
entry: Utc::now(),
description: ann.into(),
})?;
}
Ok(())
}

View file

@ -33,8 +33,8 @@ The following keys, and key formats, are defined:
* `start` - the most recent time at which this task was started (a task with no `start` key is not active)
* `tag_<tag>` - indicates this task has tag `<tag>` (value is an empty string)
* `wait` - indicates the time before which this task should be hidden, as it is not actionable
* `annotation_<timestamp>` - value is an annotation created at the given time
The following are not yet implemented:
* `dep.<uuid>` - indicates this task depends on `<uuid>` (value is an empty string)
* `annotation.<timestamp>` - value is an annotation created at the given time
* `dep_<uuid>` - indicates this task depends on `<uuid>` (value is an empty string)

View file

@ -58,7 +58,7 @@ pub use errors::Error;
pub use replica::Replica;
pub use server::{Server, ServerConfig};
pub use storage::StorageConfig;
pub use task::{Priority, Status, Tag, Task, TaskMut};
pub use task::{Annotation, Priority, Status, Tag, Task, TaskMut};
pub use workingset::WorkingSet;
/// Re-exported type from the `uuid` crate, for ease of compatibility for consumers of this crate.

View file

@ -1,7 +1,7 @@
use super::Timestamp;
/// An annotation for a task
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Annotation {
/// Time the annotation was made
pub entry: Timestamp,

View file

@ -1,5 +1,5 @@
use super::tag::{SyntheticTag, TagInner};
use super::{Status, Tag};
use super::{Annotation, Status, Tag, Timestamp};
use crate::replica::Replica;
use crate::storage::TaskMap;
use chrono::prelude::*;
@ -145,6 +145,22 @@ impl Task {
)
}
/// Iterate over the task's annotations, in arbitrary order.
pub fn get_annotations(&self) -> impl Iterator<Item = Annotation> + '_ {
self.taskmap.iter().filter_map(|(k, v)| {
if let Some(ts) = k.strip_prefix("annotation_") {
if let Ok(ts) = ts.parse::<i64>() {
return Some(Annotation {
entry: Utc.timestamp(ts, 0),
description: v.to_owned(),
});
}
// note that invalid "annotation_*" are ignored
}
None
})
}
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
self.get_timestamp("modified")
}
@ -225,6 +241,20 @@ impl<'r> TaskMut<'r> {
self.set_string(format!("tag_{}", tag), None)
}
/// Add a new annotation. Note that annotations with the same entry time
/// will overwrite one another.
pub fn add_annotation(&mut self, ann: Annotation) -> anyhow::Result<()> {
self.set_string(
format!("annotation_{}", ann.entry.timestamp()),
Some(ann.description),
)
}
/// Remove an annotation, based on its entry time.
pub fn remove_annotation(&mut self, entry: Timestamp) -> anyhow::Result<()> {
self.set_string(format!("annotation_{}", entry.timestamp()), None)
}
// -- utility functions
fn lastmod(&mut self) -> anyhow::Result<()> {
@ -442,6 +472,90 @@ mod test {
assert_eq!(tags, vec![utag("ok"), stag(SyntheticTag::Pending)]);
}
#[test]
fn test_get_annotations() {
let task = Task::new(
Uuid::new_v4(),
vec![
(
String::from("annotation_1635301873"),
String::from("left message"),
),
(
String::from("annotation_1635301883"),
String::from("left another message"),
),
(String::from("annotation_"), String::from("invalid")),
(String::from("annotation_abcde"), String::from("invalid")),
]
.drain(..)
.collect(),
);
let mut anns: Vec<_> = task.get_annotations().collect();
anns.sort();
assert_eq!(
anns,
vec![
Annotation {
entry: Utc.timestamp(1635301873, 0),
description: "left message".into()
},
Annotation {
entry: Utc.timestamp(1635301883, 0),
description: "left another message".into()
}
]
);
}
#[test]
fn test_add_annotation() {
with_mut_task(|mut task| {
task.add_annotation(Annotation {
entry: Utc.timestamp(1635301900, 0),
description: "right message".into(),
})
.unwrap();
let k = "annotation_1635301900";
assert_eq!(task.taskmap[k], "right message".to_owned());
task.reload().unwrap();
assert_eq!(task.taskmap[k], "right message".to_owned());
// adding with same time overwrites..
task.add_annotation(Annotation {
entry: Utc.timestamp(1635301900, 0),
description: "right message 2".into(),
})
.unwrap();
assert_eq!(task.taskmap[k], "right message 2".to_owned());
});
}
#[test]
fn test_remove_annotation() {
with_mut_task(|mut task| {
task.set_string("annotation_1635301873", Some("left message".into()))
.unwrap();
task.set_string("annotation_1635301883", Some("left another message".into()))
.unwrap();
task.remove_annotation(Utc.timestamp(1635301873, 0))
.unwrap();
task.reload().unwrap();
let mut anns: Vec<_> = task.get_annotations().collect();
anns.sort();
assert_eq!(
anns,
vec![Annotation {
entry: Utc.timestamp(1635301883, 0),
description: "left another message".into()
}]
);
});
}
#[test]
fn test_start() {
with_mut_task(|mut task| {