use crate::error::Result;
use crate::model::{Scope, TodoStatus};
use crate::store::Bucket;
use crate::store::DataStore;
use uuid::Uuid;
pub fn propagate_status_change<S: DataStore>(
store: &mut S,
scope: Scope,
child_parent_id: Option<Uuid>,
) -> Result<()> {
let mut current_parent_id = child_parent_id;
while let Some(parent_id) = current_parent_id {
let mut parent_pad = match store.get_pad(&parent_id, scope, Bucket::Active) {
Ok(p) => p,
Err(_) => {
break;
}
};
let all_pads = store.list_pads(scope, Bucket::Active)?;
let children: Vec<&crate::model::Pad> = all_pads
.iter()
.filter(|p| p.metadata.parent_id == Some(parent_id))
.collect();
if children.is_empty() {
break;
}
let derived = calculate_status(&children);
if parent_pad.metadata.status != derived {
parent_pad.metadata.status = derived;
parent_pad.metadata.updated_at = chrono::Utc::now();
store.save_pad(&parent_pad, scope, Bucket::Active)?;
current_parent_id = parent_pad.metadata.parent_id;
} else {
break;
}
}
Ok(())
}
fn calculate_status(children: &[&crate::model::Pad]) -> TodoStatus {
let all_done = children
.iter()
.all(|p| p.metadata.status == TodoStatus::Done);
let all_planned = children
.iter()
.all(|p| p.metadata.status == TodoStatus::Planned);
if all_done {
TodoStatus::Done
} else if all_planned {
TodoStatus::Planned
} else {
TodoStatus::InProgress
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Pad, TodoStatus};
use crate::store::bucketed::BucketedStore;
use crate::store::mem_backend::MemBackend;
fn make_pad(title: &str, status: TodoStatus) -> Pad {
let mut p = Pad::new(title.to_string(), "".to_string());
p.metadata.status = status;
p
}
#[test]
fn test_propagate_all_planned() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let parent = make_pad("Parent", TodoStatus::Done); let mut child1 = make_pad("Child1", TodoStatus::Planned);
let mut child2 = make_pad("Child2", TodoStatus::Planned);
let parent_id = parent.metadata.id;
child1.metadata.parent_id = Some(parent_id);
child2.metadata.parent_id = Some(parent_id);
store
.save_pad(&parent, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child1, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child2, Scope::Project, Bucket::Active)
.unwrap();
propagate_status_change(&mut store, Scope::Project, Some(parent_id)).unwrap();
let updated_parent = store
.get_pad(&parent_id, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(updated_parent.metadata.status, TodoStatus::Planned);
}
#[test]
fn test_propagate_all_done() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let parent = make_pad("Parent", TodoStatus::Planned);
let mut child1 = make_pad("Child1", TodoStatus::Done);
let mut child2 = make_pad("Child2", TodoStatus::Done);
let parent_id = parent.metadata.id;
child1.metadata.parent_id = Some(parent_id);
child2.metadata.parent_id = Some(parent_id);
store
.save_pad(&parent, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child1, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child2, Scope::Project, Bucket::Active)
.unwrap();
propagate_status_change(&mut store, Scope::Project, Some(parent_id)).unwrap();
let updated_parent = store
.get_pad(&parent_id, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(updated_parent.metadata.status, TodoStatus::Done);
}
#[test]
fn test_propagate_mixed_done_planned() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let parent = make_pad("Parent", TodoStatus::Planned);
let mut child1 = make_pad("Child1", TodoStatus::Done);
let mut child2 = make_pad("Child2", TodoStatus::Planned);
let parent_id = parent.metadata.id;
child1.metadata.parent_id = Some(parent_id);
child2.metadata.parent_id = Some(parent_id);
store
.save_pad(&parent, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child1, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child2, Scope::Project, Bucket::Active)
.unwrap();
propagate_status_change(&mut store, Scope::Project, Some(parent_id)).unwrap();
let updated_parent = store
.get_pad(&parent_id, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(updated_parent.metadata.status, TodoStatus::InProgress);
}
#[test]
fn test_propagate_ignores_deleted_children() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let parent = make_pad("Parent", TodoStatus::Planned);
let mut child1 = make_pad("Child1", TodoStatus::Done);
let mut child2 = make_pad("Child2", TodoStatus::Planned);
let parent_id = parent.metadata.id;
child1.metadata.parent_id = Some(parent_id);
child2.metadata.parent_id = Some(parent_id);
store
.save_pad(&parent, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child1, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child2, Scope::Project, Bucket::Deleted)
.unwrap();
propagate_status_change(&mut store, Scope::Project, Some(parent_id)).unwrap();
let updated_parent = store
.get_pad(&parent_id, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(updated_parent.metadata.status, TodoStatus::Done);
}
#[test]
fn test_propagate_recursive() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let mut grandparent = make_pad("GP", TodoStatus::Planned);
let mut parent = make_pad("Parent", TodoStatus::Planned);
let mut child = make_pad("Child", TodoStatus::Done);
let gp_id = grandparent.metadata.id;
let p_id = parent.metadata.id;
grandparent.metadata.parent_id = None;
parent.metadata.parent_id = Some(gp_id);
child.metadata.parent_id = Some(p_id);
store
.save_pad(&grandparent, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&parent, Scope::Project, Bucket::Active)
.unwrap();
store
.save_pad(&child, Scope::Project, Bucket::Active)
.unwrap();
propagate_status_change(&mut store, Scope::Project, Some(p_id)).unwrap();
let updated_p = store
.get_pad(&p_id, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(updated_p.metadata.status, TodoStatus::Done);
let updated_gp = store
.get_pad(&gp_id, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(updated_gp.metadata.status, TodoStatus::Done);
}
}