Skip to content

Commit

Permalink
Merge pull request #66 from Jim-Hodapp-Coaching/update_statuses
Browse files Browse the repository at this point in the history
Update statuses
  • Loading branch information
calebbourg committed Sep 26, 2024
2 parents dd67b67 + 4f64c66 commit c54ffb8
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 5 deletions.
2 changes: 2 additions & 0 deletions docs/db/refactor_platform_rs.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Table refactor_platform.overarching_goals {
coaching_session_id uuid [note: 'The coaching session that an overarching goal is associated with']
title varchar [note: 'A short description of an overarching goal']
body varchar [note: 'Main text of the overarching goal supporting Markdown']
status status [not null]
status_changed_at timestamptz
completed_at timestamptz [note: 'The date and time an overarching goal was completed']
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`, note: 'The last date and time fields were changed']
Expand Down
Binary file modified docs/db/refactor_platform_rs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions entity/src/overarching_goals.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3

use crate::status::Status;
use crate::Id;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
Expand All @@ -17,6 +18,10 @@ pub struct Model {
pub title: Option<String>,
pub body: Option<String>,
#[serde(skip_deserializing)]
pub status: Status,
#[serde(skip_deserializing)]
pub status_changed_at: Option<DateTimeWithTimeZone>,
#[serde(skip_deserializing)]
pub completed_at: Option<DateTimeWithTimeZone>,
#[serde(skip_deserializing)]
pub created_at: DateTimeWithTimeZone,
Expand Down
12 changes: 12 additions & 0 deletions entity/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ impl std::default::Default for Status {
Self::InProgress
}
}

impl From<&str> for Status {
fn from(value: &str) -> Self {
match value {
"not_started" => Self::NotStarted,
"in_progress" => Self::InProgress,
"completed" => Self::Completed,
"wont_do" => Self::WontDo,
_ => Self::InProgress,
}
}
}
91 changes: 90 additions & 1 deletion entity_api/src/action.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::error::{EntityApiErrorCode, Error};
use crate::uuid_parse_str;
use entity::actions::{self, ActiveModel, Entity, Model};
use entity::Id;
use entity::{status::Status, Id};
use sea_orm::{
entity::prelude::*,
ActiveValue::{Set, Unchanged},
Expand Down Expand Up @@ -65,6 +65,42 @@ pub async fn update(db: &DatabaseConnection, id: Id, model: Model) -> Result<Mod
}
}

pub async fn update_status(
db: &DatabaseConnection,
id: Id,
status: Status,
) -> Result<Model, Error> {
let result = Entity::find_by_id(id).one(db).await?;

match result {
Some(action) => {
debug!("Existing Action model to be Updated: {:?}", action);

let active_model: ActiveModel = ActiveModel {
id: Unchanged(action.id),
coaching_session_id: Unchanged(action.coaching_session_id),
user_id: Unchanged(action.user_id),
body: Unchanged(action.body),
due_by: Unchanged(action.due_by),
status: Set(status),
status_changed_at: Set(Some(chrono::Utc::now().into())),
updated_at: Set(chrono::Utc::now().into()),
created_at: Unchanged(action.created_at),
};

Ok(active_model.update(db).await?.try_into_model()?)
}
None => {
error!("Action with id {} not found", id);

Err(Error {
inner: None,
error_code: EntityApiErrorCode::RecordNotFound,
})
}
}
}

pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result<Option<Model>, Error> {
match Entity::find_by_id(id).one(db).await {
Ok(Some(action)) => {
Expand Down Expand Up @@ -179,6 +215,59 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn update_status_returns_an_updated_action_model() -> Result<(), Error> {
let now = chrono::Utc::now();

let action_model = Model {
id: Id::new_v4(),
coaching_session_id: Id::new_v4(),
due_by: Some(now.into()),
body: Some("This is a action".to_owned()),
user_id: Id::new_v4(),
status_changed_at: None,
status: Default::default(),
created_at: now.into(),
updated_at: now.into(),
};

let updated_action_model = Model {
id: Id::new_v4(),
coaching_session_id: Id::new_v4(),
due_by: Some(now.into()),
body: Some("This is a action".to_owned()),
user_id: Id::new_v4(),
status_changed_at: None,
status: Status::Completed,
created_at: now.into(),
updated_at: now.into(),
};

let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results(vec![
vec![action_model.clone()],
vec![updated_action_model.clone()],
])
.into_connection();

let action = update_status(&db, action_model.id, Status::Completed).await?;

assert_eq!(action.status, Status::Completed);

Ok(())
}

#[tokio::test]
async fn update_status_returns_error_when_action_not_found() -> Result<(), Error> {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();

let result = update_status(&db, Id::new_v4(), Status::Completed).await;

assert_eq!(result.is_err(), true);

Ok(())
}

#[tokio::test]
async fn find_by_returns_all_actions_associated_with_coaching_session() -> Result<(), Error> {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
Expand Down
106 changes: 104 additions & 2 deletions entity_api/src/overarching_goal.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::error::{EntityApiErrorCode, Error};
use crate::uuid_parse_str;
use entity::overarching_goals::{self, ActiveModel, Entity, Model};
use entity::Id;
use entity::{status::Status, Id};
use sea_orm::{
entity::prelude::*,
ActiveModelTrait,
Expand Down Expand Up @@ -56,6 +56,8 @@ pub async fn update(db: &DatabaseConnection, id: Id, model: Model) -> Result<Mod
user_id: Unchanged(model.user_id),
body: Set(model.body),
title: Set(model.title),
status: Set(model.status),
status_changed_at: Set(model.status_changed_at),
completed_at: Set(model.completed_at),
updated_at: Set(chrono::Utc::now().into()),
created_at: Unchanged(model.created_at),
Expand All @@ -74,6 +76,46 @@ pub async fn update(db: &DatabaseConnection, id: Id, model: Model) -> Result<Mod
}
}

pub async fn update_status(
db: &DatabaseConnection,
id: Id,
status: Status,
) -> Result<Model, Error> {
let result = Entity::find_by_id(id).one(db).await?;

match result {
Some(overarching_goal) => {
debug!(
"Existing Overarching Goal model to be Updated: {:?}",
overarching_goal
);

let active_model: ActiveModel = ActiveModel {
id: Unchanged(overarching_goal.id),
coaching_session_id: Unchanged(overarching_goal.coaching_session_id),
user_id: Unchanged(overarching_goal.user_id),
body: Unchanged(overarching_goal.body),
title: Unchanged(overarching_goal.title),
status: Set(status),
status_changed_at: Set(Some(chrono::Utc::now().into())),
completed_at: Unchanged(overarching_goal.completed_at),
updated_at: Set(chrono::Utc::now().into()),
created_at: Unchanged(overarching_goal.created_at),
};

Ok(active_model.update(db).await?.try_into_model()?)
}
None => {
error!("Overarching Goal with id {} not found", id);

Err(Error {
inner: None,
error_code: EntityApiErrorCode::RecordNotFound,
})
}
}
}

pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result<Option<Model>, Error> {
match Entity::find_by_id(id).one(db).await {
Ok(Some(overarching_goal)) => {
Expand Down Expand Up @@ -148,6 +190,8 @@ mod tests {
coaching_session_id: Some(Id::new_v4()),
title: Some("title".to_owned()),
body: Some("This is a overarching_goal".to_owned()),
status_changed_at: None,
status: Default::default(),
completed_at: Some(now.into()),
created_at: now.into(),
updated_at: now.into(),
Expand Down Expand Up @@ -176,6 +220,8 @@ mod tests {
body: Some("This is a overarching_goal".to_owned()),
user_id: Id::new_v4(),
completed_at: Some(now.into()),
status_changed_at: None,
status: Default::default(),
created_at: now.into(),
updated_at: now.into(),
};
Expand All @@ -199,6 +245,62 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn update_status_returns_an_updated_overarching_goal_model() -> Result<(), Error> {
let now = chrono::Utc::now();

let overarching_goal_model = Model {
id: Id::new_v4(),
coaching_session_id: Some(Id::new_v4()),
title: Some("title".to_owned()),
body: Some("This is a overarching_goal".to_owned()),
user_id: Id::new_v4(),
completed_at: Some(now.into()),
status_changed_at: None,
status: Default::default(),
created_at: now.into(),
updated_at: now.into(),
};

let updated_overarching_goal_model = Model {
id: Id::new_v4(),
coaching_session_id: Some(Id::new_v4()),
title: Some("title".to_owned()),
body: Some("This is a overarching_goal".to_owned()),
user_id: Id::new_v4(),
completed_at: Some(now.into()),
status_changed_at: Some(now.into()),
status: Status::Completed,
created_at: now.into(),
updated_at: now.into(),
};

let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results(vec![
vec![overarching_goal_model.clone()],
vec![updated_overarching_goal_model.clone()],
])
.into_connection();

let overarching_goal =
update_status(&db, overarching_goal_model.id, Status::Completed).await?;

assert_eq!(overarching_goal.status, Status::Completed);

Ok(())
}

#[tokio::test]
async fn update_status_returns_error_when_overarching_goal_not_found() -> Result<(), Error> {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();

let result = update_status(&db, Id::new_v4(), Status::Completed).await;

assert_eq!(result.is_err(), true);

Ok(())
}

#[tokio::test]
async fn find_by_returns_all_overarching_goals_associated_with_coaching_session(
) -> Result<(), Error> {
Expand All @@ -217,7 +319,7 @@ mod tests {
db.into_transaction_log(),
[Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "overarching_goals"."id", "overarching_goals"."coaching_session_id", "overarching_goals"."user_id", "overarching_goals"."title", "overarching_goals"."body", "overarching_goals"."completed_at", "overarching_goals"."created_at", "overarching_goals"."updated_at" FROM "refactor_platform"."overarching_goals" WHERE "overarching_goals"."coaching_session_id" = $1"#,
r#"SELECT "overarching_goals"."id", "overarching_goals"."coaching_session_id", "overarching_goals"."user_id", "overarching_goals"."title", "overarching_goals"."body", CAST("overarching_goals"."status" AS text), "overarching_goals"."status_changed_at", "overarching_goals"."completed_at", "overarching_goals"."created_at", "overarching_goals"."updated_at" FROM "refactor_platform"."overarching_goals" WHERE "overarching_goals"."coaching_session_id" = $1"#,
[coaching_session_id.into()]
)]
);
Expand Down
6 changes: 4 additions & 2 deletions migration/src/refactor_platform_rs.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- SQL dump generated using DBML (dbml-lang.org)
-- SQL dump generated using DBML (dbml.dbdiagram.io)
-- Database: PostgreSQL
-- Generated at: 2024-09-21T03:25:20.880Z
-- Generated at: 2024-09-25T08:10:38.833Z


CREATE TYPE "status" AS ENUM (
Expand Down Expand Up @@ -55,6 +55,8 @@ CREATE TABLE "refactor_platform"."overarching_goals" (
"coaching_session_id" uuid,
"title" varchar,
"body" varchar,
"status" status NOT NULL,
"status_changed_at" timestamptz,
"completed_at" timestamptz,
"created_at" timestamptz NOT NULL DEFAULT (now()),
"updated_at" timestamptz NOT NULL DEFAULT (now())
Expand Down
35 changes: 35 additions & 0 deletions web/src/controller/action_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,41 @@ pub async fn update(
Ok(Json(ApiResponse::new(StatusCode::OK.into(), action)))
}

#[utoipa::path(
put,
path = "/actions/{id}/status",
params(
ApiVersion,
("id" = Id, Path, description = "Id of action to update"),
("value" = Option<String>, Query, description = "Status value to update"),
),
request_body = entity::actions::Model,
responses(
(status = 200, description = "Successfully Updated Action", body = [entity::actions::Model]),
(status = 401, description = "Unauthorized"),
(status = 405, description = "Method not allowed")
),
security(
("cookie_auth" = [])
)
)]
pub async fn update_status(
CompareApiVersion(_v): CompareApiVersion,
AuthenticatedUser(_user): AuthenticatedUser,
Query(status): Query<String>,
Path(id): Path<Id>,
State(app_state): State<AppState>,
) -> Result<impl IntoResponse, Error> {
debug!("PUT Update Action Status with id: {}", id);

let action =
ActionApi::update_status(app_state.db_conn_ref(), id, status.as_str().into()).await?;

debug!("Updated Action: {:?}", action);

Ok(Json(ApiResponse::new(StatusCode::OK.into(), action)))
}

#[utoipa::path(
get,
path = "/actions",
Expand Down
Loading

0 comments on commit c54ffb8

Please sign in to comment.