Merge pull request #330 from djmitche/issue327

Support an 'end' key in task maps
This commit is contained in:
Dustin J. Mitchell 2022-01-24 10:24:18 -05:00 committed by GitHub
commit 8576e7ffa7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 93 additions and 4 deletions

View file

@ -17,6 +17,12 @@ The result of this reconciliation will be `oldtag,newtag2`, while the user almos
The key names given below avoid this issue, allowing user updates such as adding a tag or deleting a dependency to be represented in a single `Update` operation. The key names given below avoid this issue, allowing user updates such as adding a tag or deleting a dependency to be represented in a single `Update` operation.
## Validity
_Any_ key/value map is a valid task.
Consumers of task data must make a best effort to interpret any map, even if it contains apparently contradictory information.
For example, a task with status "completed" but no "end" key present should be interpreted as completed at an unknown time.
## Representations ## Representations
Integers are stored in decimal notation. Integers are stored in decimal notation.
@ -31,6 +37,7 @@ The following keys, and key formats, are defined:
* `description` - the one-line summary of the task * `description` - the one-line summary of the task
* `modified` - the time of the last modification of this task * `modified` - the time of the last modification of this task
* `start` - the most recent time at which this task was started (a task with no `start` key is not active) * `start` - the most recent time at which this task was started (a task with no `start` key is not active)
* `end` - if present, the time at which this task was completed or deleted (note that this key may not agree with `status`: it may be present for a pending task, or absent for a deleted or completed task)
* `tag_<tag>` - indicates this task has tag `<tag>` (value is an empty string) * `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 * `wait` - indicates the time before which this task should be hidden, as it is not actionable
* `entry` - the time at which the task was created * `entry` - the time at which the task was created

View file

@ -241,7 +241,9 @@ mod tests {
.. ..
} = op } = op
{ {
if property == "modified" || property == "entry" { // rewrite automatically-created dates to "just-now" for ease
// of testing
if property == "modified" || property == "end" || property == "entry" {
if value.is_some() { if value.is_some() {
value = Some("just-now".into()); value = Some("just-now".into());
} }
@ -311,6 +313,13 @@ mod tests {
value: Some("past tense".into()), value: Some("past tense".into()),
timestamp: now, timestamp: now,
}, },
ReplicaOp::Update {
uuid: t.get_uuid(),
property: "end".into(),
old_value: None,
value: Some("just-now".into()),
timestamp: now,
},
ReplicaOp::Update { ReplicaOp::Update {
uuid: t.get_uuid(), uuid: t.get_uuid(),
property: "status".into(), property: "status".into(),

View file

@ -57,6 +57,7 @@ enum Prop {
Start, Start,
Status, Status,
Wait, Wait,
End,
Entry, Entry,
} }
@ -271,9 +272,22 @@ impl<'r> TaskMut<'r> {
/// Set the task's status. This also adds the task to the working set if the /// Set the task's status. This also adds the task to the working set if the
/// new status puts it in that set. /// new status puts it in that set.
pub fn set_status(&mut self, status: Status) -> anyhow::Result<()> { pub fn set_status(&mut self, status: Status) -> anyhow::Result<()> {
if status == Status::Pending { match status {
let uuid = self.uuid; Status::Pending => {
self.replica.add_to_working_set(uuid)?; // clear "end" when a task becomes "pending"
if self.taskmap.contains_key(Prop::End.as_ref()) {
self.set_timestamp(Prop::End.as_ref(), None)?;
}
let uuid = self.uuid;
self.replica.add_to_working_set(uuid)?;
}
Status::Completed | Status::Deleted => {
// set "end" when a task is deleted or completed
if !self.taskmap.contains_key(Prop::End.as_ref()) {
self.set_timestamp(Prop::End.as_ref(), Some(Utc::now()))?;
}
}
_ => {}
} }
self.set_string( self.set_string(
Prop::Status.as_ref(), Prop::Status.as_ref(),
@ -316,6 +330,14 @@ impl<'r> TaskMut<'r> {
self.set_status(Status::Completed) self.set_status(Status::Completed)
} }
/// Mark this task as deleted.
///
/// Note that this does not delete the task. It merely marks the task as
/// deleted.
pub fn delete(&mut self) -> anyhow::Result<()> {
self.set_status(Status::Deleted)
}
/// Add a tag to this task. Does nothing if the tag is already present. /// Add a tag to this task. Does nothing if the tag is already present.
pub fn add_tag(&mut self, tag: &Tag) -> anyhow::Result<()> { pub fn add_tag(&mut self, tag: &Tag) -> anyhow::Result<()> {
if tag.is_synthetic() { if tag.is_synthetic() {
@ -688,6 +710,41 @@ mod test {
}); });
} }
#[test]
fn test_set_status_pending() {
with_mut_task(|mut task| {
task.done().unwrap();
task.set_status(Status::Pending).unwrap();
assert_eq!(task.get_status(), Status::Pending);
assert!(!task.taskmap.contains_key("end"));
assert!(task.has_tag(&stag(SyntheticTag::Pending)));
assert!(!task.has_tag(&stag(SyntheticTag::Completed)));
});
}
#[test]
fn test_set_status_completed() {
with_mut_task(|mut task| {
task.set_status(Status::Completed).unwrap();
assert_eq!(task.get_status(), Status::Completed);
assert!(task.taskmap.contains_key("end"));
assert!(!task.has_tag(&stag(SyntheticTag::Pending)));
assert!(task.has_tag(&stag(SyntheticTag::Completed)));
});
}
#[test]
fn test_set_status_deleted() {
with_mut_task(|mut task| {
task.set_status(Status::Deleted).unwrap();
assert_eq!(task.get_status(), Status::Deleted);
assert!(task.taskmap.contains_key("end"));
assert!(!task.has_tag(&stag(SyntheticTag::Pending)));
assert!(!task.has_tag(&stag(SyntheticTag::Completed)));
});
}
#[test] #[test]
fn test_start() { fn test_start() {
with_mut_task(|mut task| { with_mut_task(|mut task| {
@ -730,6 +787,7 @@ mod test {
with_mut_task(|mut task| { with_mut_task(|mut task| {
task.done().unwrap(); task.done().unwrap();
assert_eq!(task.get_status(), Status::Completed); assert_eq!(task.get_status(), Status::Completed);
assert!(task.taskmap.contains_key("end"));
assert!(task.has_tag(&stag(SyntheticTag::Completed))); assert!(task.has_tag(&stag(SyntheticTag::Completed)));
// redundant call does nothing.. // redundant call does nothing..
@ -739,6 +797,21 @@ mod test {
}); });
} }
#[test]
fn test_delete() {
with_mut_task(|mut task| {
task.delete().unwrap();
assert_eq!(task.get_status(), Status::Deleted);
assert!(task.taskmap.contains_key("end"));
assert!(!task.has_tag(&stag(SyntheticTag::Completed)));
// redundant call does nothing..
task.delete().unwrap();
assert_eq!(task.get_status(), Status::Deleted);
assert!(!task.has_tag(&stag(SyntheticTag::Completed)));
});
}
#[test] #[test]
fn test_add_tags() { fn test_add_tags() {
with_mut_task(|mut task| { with_mut_task(|mut task| {