mod task_list;
mod task_note;
mod task_priority;
mod task_resolution;
mod task_status;
mod task_work_log;
use chrono::{DateTime, Duration, Utc};
use eyre::{Result, eyre};
use serde::{Deserialize, Serialize};
pub use task_list::TaskList;
pub use task_note::TaskNote;
pub use task_priority::TaskPriority;
pub use task_resolution::TaskResolution;
pub use task_status::TaskStatus;
pub use task_work_log::TaskWorkLog;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct Task {
pub completed_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub description: Option<String>,
pub due_at: Option<DateTime<Utc>>,
pub friendly_id: String,
pub id: u32,
pub notes: Vec<TaskNote>,
pub priority: TaskPriority,
pub resolution: Option<TaskResolution>,
pub status: TaskStatus,
pub subject: String,
pub tags: Vec<String>,
pub updated_at: DateTime<Utc>,
pub work_logs: Vec<TaskWorkLog>,
}
impl Task {
pub fn new(id: u32, friendly_id: impl Into<String>, subject: impl Into<String>) -> Self {
let now = Utc::now();
Self {
completed_at: None,
created_at: now,
description: None,
due_at: None,
friendly_id: friendly_id.into(),
id,
notes: Vec::new(),
priority: TaskPriority::default(),
resolution: None,
status: TaskStatus::default(),
subject: subject.into(),
tags: Vec::new(),
updated_at: now,
work_logs: Vec::new(),
}
}
pub fn add_description(&mut self, description: impl Into<String>) {
self.description = Some(description.into());
self.touch();
}
pub fn add_note(&mut self, note: impl Into<String>) {
let note = TaskNote::new(note);
self.notes.push(note);
self.touch();
}
pub fn add_tag(&mut self, tag: impl Into<String>) {
let tag = tag.into();
if !self.has_tag(&tag) {
self.tags.push(tag);
self.touch();
}
}
pub fn cancel(&mut self) {
if !self.is_done() {
self.status = TaskStatus::Done;
self.resolution = Some(TaskResolution::Cancelled);
self.touch();
}
}
pub fn cancel_with_note(&mut self, note: impl Into<String>) {
if !self.is_done() {
self.cancel();
self.add_note(note);
}
}
pub fn complete(&mut self) {
if !self.is_done() {
self.completed_at = Some(Utc::now());
self.status = TaskStatus::Done;
self.resolution = Some(TaskResolution::Completed);
self.touch();
}
}
pub fn complete_with_note(&mut self, note: impl Into<String>) {
if !self.is_done() {
self.complete();
self.add_note(note);
}
}
pub fn delegate(&mut self) {
if !self.is_done() {
self.status = TaskStatus::Done;
self.resolution = Some(TaskResolution::Delegated);
self.touch();
}
}
pub fn delegate_with_note(&mut self, note: impl Into<String>) {
if !self.is_done() {
self.delegate();
self.add_note(note);
}
}
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.contains(&tag.to_string())
}
pub fn is_being_worked(&self) -> bool {
self.work_logs.iter().any(|wl| wl.ended_at.is_none())
}
pub fn is_cancelled(&self) -> bool {
matches!(self.resolution, Some(TaskResolution::Cancelled)) && self.is_done()
}
pub fn is_complete(&self) -> bool {
matches!(self.resolution, Some(TaskResolution::Completed)) && self.is_done()
}
pub fn is_delegated(&self) -> bool {
matches!(self.resolution, Some(TaskResolution::Delegated)) && self.is_done()
}
pub fn is_done(&self) -> bool {
self.status == TaskStatus::Done
}
pub fn is_in_progress(&self) -> bool {
self.status == TaskStatus::InProgress
}
pub fn is_overdue(&self) -> bool {
self.due_at.is_some_and(|due| due < Utc::now())
}
pub fn is_todo(&self) -> bool {
self.status == TaskStatus::Todo
}
pub fn remove_tag(&mut self, tag: &str) -> bool {
let initial_len = self.tags.len();
self.tags.retain(|t| t != tag);
if self.tags.len() == initial_len {
false
} else {
self.touch();
true
}
}
pub fn reopen(&mut self) {
if self.is_done() {
self.completed_at = None;
self.status = TaskStatus::Todo;
self.resolution = None;
self.touch();
}
}
pub fn reopen_with_note(&mut self, note: impl Into<String>) {
if self.is_done() {
self.reopen();
self.add_note(note);
}
}
pub fn set_due_date(&mut self, due_date: impl Into<DateTime<Utc>>) -> Result<()> {
let due_date = due_date.into();
if due_date < Utc::now() {
return Err(eyre!("Due date cannot be in the past"));
}
self.due_at = Some(due_date);
self.touch();
Ok(())
}
pub fn set_priority(&mut self, priority: TaskPriority) {
if self.priority != priority {
self.priority = priority;
self.touch();
}
}
pub fn start_work(&mut self) -> Result<()> {
self.start_work_impl(None, None)
}
pub fn start_work_with_note(&mut self, note: impl Into<String>) -> Result<()> {
self.start_work_impl(Some(¬e.into()), None)
}
pub fn start_work_with_note_and_source(
&mut self,
note: impl Into<String>,
source: impl Into<String>,
) -> Result<()> {
self.start_work_impl(Some(¬e.into()), Some(&source.into()))
}
pub fn start_work_with_source(&mut self, source: impl Into<String>) -> Result<()> {
self.start_work_impl(None, Some(&source.into()))
}
pub fn stop_work(&mut self) {
self
.work_logs
.iter_mut()
.filter(|wl| wl.ended_at.is_none())
.for_each(TaskWorkLog::stop);
self.touch();
}
pub fn total_time_worked(&self) -> Duration {
self
.work_logs
.iter()
.filter_map(task_work_log::TaskWorkLog::duration)
.sum()
}
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn update_friendly_id(&mut self, friendly_id: impl Into<String>) {
self.friendly_id = friendly_id.into();
self.touch();
}
fn start_work_impl(&mut self, note: Option<&str>, source: Option<&str>) -> Result<()> {
if self.status == TaskStatus::Done {
return Err(eyre!("Task is already done"));
}
if self.status != TaskStatus::InProgress {
self.status = TaskStatus::InProgress;
}
self.stop_work();
let work_log = match (note, source) {
(Some(note), Some(source)) => TaskWorkLog::new().with_note(note).with_source(source),
(Some(note), None) => TaskWorkLog::new().with_note(note),
(None, Some(source)) => TaskWorkLog::new().with_source(source),
(None, None) => TaskWorkLog::new(),
};
self.work_logs.push(work_log);
self.touch();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_task() -> Task {
Task::new(1, "1", "a test task")
}
mod new {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_creates_a_new_task() {
let task = Task::new(1, "1", "a test task");
assert_eq!(task.completed_at, None);
assert!(task.created_at.timestamp() > 0);
assert_eq!(task.description, None);
assert_eq!(task.due_at, None);
assert_eq!(task.friendly_id, "1");
assert_eq!(task.id, 1);
assert_eq!(task.notes, Vec::new());
assert_eq!(task.priority, TaskPriority::default());
assert_eq!(task.resolution, None);
assert_eq!(task.status, TaskStatus::default());
assert_eq!(task.subject, "a test task");
assert_eq!(task.tags, Vec::<String>::new());
assert!(task.updated_at.timestamp() > 0);
assert_eq!(task.work_logs, Vec::new());
}
}
mod add_description {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_adds_a_description() {
let mut task = create_task();
task.add_description("a test description");
assert_eq!(task.description, Some("a test description".to_string()));
}
}
mod add_note {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_adds_a_note() {
let mut task = create_task();
task.add_note("a test note");
assert_eq!(task.notes.len(), 1);
assert_eq!(task.notes[0].content, "a test note");
}
}
mod add_tag {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_adds_a_tag() {
let mut task = create_task();
task.add_tag("a test tag");
assert_eq!(task.tags.len(), 1);
assert_eq!(task.tags[0], "a test tag");
}
#[test]
fn it_does_not_add_a_duplicate_tag() {
let mut task = create_task();
task.add_tag("a test tag");
task.add_tag("a test tag");
assert_eq!(task.tags.len(), 1);
}
}
mod cancel {
use pretty_assertions::{assert_eq, assert_ne};
use super::*;
#[test]
fn it_cancels_the_task() {
let mut task = create_task();
task.cancel();
assert_eq!(task.status, TaskStatus::Done);
assert_eq!(task.resolution, Some(TaskResolution::Cancelled));
}
#[test]
fn it_does_not_cancel_the_task_if_already_done() {
let mut task = create_task();
task.complete();
task.cancel();
assert_ne!(task.resolution, Some(TaskResolution::Cancelled));
}
}
mod cancel_with_note {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_cancels_the_task_with_a_note() {
let mut task = create_task();
task.cancel_with_note("a test note");
assert!(task.is_cancelled());
assert_eq!(task.notes.len(), 1);
assert_eq!(task.notes[0].content, "a test note");
}
}
mod complete {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_completes_the_task() {
let mut task = create_task();
task.complete();
assert!(task.completed_at.is_some());
assert_eq!(task.status, TaskStatus::Done);
assert_eq!(task.resolution, Some(TaskResolution::Completed));
}
#[test]
fn it_does_not_complete_the_task_if_already_done() {
let mut task = create_task();
task.complete();
let completed_at = task.completed_at.unwrap();
task.complete();
assert_eq!(task.completed_at, Some(completed_at));
}
}
mod complete_with_note {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_cancels_the_task_with_a_note() {
let mut task = create_task();
task.complete_with_note("a test note");
assert!(task.is_complete());
assert_eq!(task.notes.len(), 1);
assert_eq!(task.notes[0].content, "a test note");
}
}
}