From 24fc948e94642c5d5bddf9a1a243f8b72f73b243 Mon Sep 17 00:00:00 2001 From: Nicolas Auler Date: Tue, 19 Dec 2023 19:20:26 -0300 Subject: [PATCH 1/2] Trying to present in home page a expense table that allows for row editiing. --- Cargo.lock | 1 + Cargo.toml | 1 + migrations/1_schema.sql | 11 ++- src/main.rs | 189 +++++++++++++++++++++++++++++++++++----- templates/expenses.html | 7 +- templates/hello.html | 12 --- 6 files changed, 185 insertions(+), 36 deletions(-) delete mode 100644 templates/hello.html diff --git a/Cargo.lock b/Cargo.lock index 2b30687..cd91148 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -649,6 +649,7 @@ dependencies = [ "shuttle-runtime", "shuttle-shared-db", "sqlx", + "strum 0.25.0", "tokio", "tower-http 0.5.0", "tracing", diff --git a/Cargo.toml b/Cargo.toml index aa2227f..b25b4d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ shuttle-axum = { version = "0.35.1", default-features = false, features = ["axum shuttle-runtime = "0.35.1" shuttle-shared-db = { version = "0.35.1", features = ["postgres"] } sqlx = { version = "0.7.3", features = ["runtime-tokio-rustls", "postgres"] } +strum = { version = "0.25.0", features = ["strum_macros", "derive"] } tokio = { version = "1.35.0", features = ["full"] } tower-http = { version = "0.5.0", features = ["fs"] } tracing = "0.1.40" diff --git a/migrations/1_schema.sql b/migrations/1_schema.sql index b6733e9..7753487 100644 --- a/migrations/1_schema.sql +++ b/migrations/1_schema.sql @@ -1,6 +1,15 @@ DROP TABLE IF EXISTS expenses; +DO $$ BEGIN +CREATE TYPE expense_type AS ENUM ('food', 'transport', 'health', 'education', 'entertainment', 'other'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + CREATE TABLE IF NOT EXISTS expenses ( id serial PRIMARY KEY, - amount real NOT NULL + description varchar(255) NOT NULL, + price real NOT NULL, + expense_type expense_type NOT NULL, + is_essencial boolean NOT NULL ); diff --git a/src/main.rs b/src/main.rs index c197665..b250b20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{fmt::Display, str::FromStr, sync::Arc}; use askama::Template; use axum::{ @@ -9,19 +9,63 @@ use axum::{ Router, //Json, }; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use shuttle_runtime::CustomError; use sqlx::{FromRow, PgPool}; +use strum::EnumIter; use tower_http::services::ServeDir; struct AppState { pool: PgPool, } +#[derive(Serialize, Clone, EnumIter, Deserialize, sqlx::Type)] +#[sqlx(type_name = "expense_type", rename_all = "lowercase")] +enum ExpenseType { + Food, + Transport, + Health, + Education, + Entertainment, + Other, +} + +impl FromStr for ExpenseType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "Food" => Ok(ExpenseType::Food), + "Transport" => Ok(ExpenseType::Transport), + "Health" => Ok(ExpenseType::Health), + "Education" => Ok(ExpenseType::Education), + "Entertainment" => Ok(ExpenseType::Entertainment), + "Other" => Ok(ExpenseType::Other), + _ => Err(format!("{} is not a valid expense type", s)), + } + } +} + +impl Display for ExpenseType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExpenseType::Food => write!(f, "Food"), + ExpenseType::Transport => write!(f, "Transport"), + ExpenseType::Health => write!(f, "Health"), + ExpenseType::Education => write!(f, "Education"), + ExpenseType::Entertainment => write!(f, "Entertainment"), + ExpenseType::Other => write!(f, "Other"), + } + } +} + #[derive(FromRow, Serialize)] struct Expense { id: i32, - amount: f32, + description: String, + price: f32, + expense_type: ExpenseType, + is_essencial: bool, } #[shuttle_runtime::main] @@ -35,8 +79,8 @@ async fn axum(#[shuttle_shared_db::Postgres] pool: PgPool) -> shuttle_axum::Shut let router = Router::new() .route("/", get(expenses_index)) .route("/expenses", get(get_expenses)) - .route("/hello", get(hello_world)) - .route("/greet/:name", get(greet)) + .route("/expenses/edit", get(edit_expenses)) + .route("/expenses/:id", get(get_expense).put(update_expense)) //.route("/whatever", get(whatever)) .nest_service("/static", ServeDir::new("./css")) .with_state(shared_state); @@ -56,20 +100,6 @@ async fn axum(#[shuttle_shared_db::Postgres] pool: PgPool) -> shuttle_axum::Shut // } //} -async fn hello_world() -> &'static str { - "Hello, world!" -} - -#[derive(Template)] -#[template(path = "hello.html")] -struct HelloTemplate { - name: String, -} - -async fn greet(Path(name): Path) -> impl IntoResponse { - HelloTemplate { name } -} - #[derive(Template)] #[template(path = "expenses.html")] struct ExpensesTemplate {} @@ -78,6 +108,62 @@ async fn expenses_index() -> impl IntoResponse { ExpensesTemplate {} } +macro_rules! TABLE_ROW { + () => { + " + {} + {} + {} + {} + + + + " + }; +} + +macro_rules! EDITABLE_TABLE_ROW { + () => { + " + + + + + + + + + " + }; +} + async fn get_expenses(State(shared_state): State>) -> impl IntoResponse { // let expenses: Vec = sqlx::query_as!(Expense, "SELECT * FROM expenses") // .fetch_all(&shared_state.pool) @@ -93,11 +179,72 @@ async fn get_expenses(State(shared_state): State>) -> impl IntoRes .iter() .map(|expense| { format!( - "{}{}", - expense.id, expense.amount + TABLE_ROW!(), + expense.description, + expense.price, + expense.expense_type, + expense.is_essencial, + expense.id ) }) .collect::>() .join("\n"), ) } + +async fn edit_expenses(State(shared_state): State>) -> impl IntoResponse { + let expenses: Vec = sqlx::query_as("SELECT * FROM expenses") + .fetch_all(&shared_state.pool) + .await + .unwrap(); + + Html( + expenses + .iter() + .map(|expense| { + format!( + EDITABLE_TABLE_ROW!(), + expense.id, + expense.description, + expense.price, + expense.is_essencial, + expense.id, + expense.id + ) + }) + .collect::>() + .join("\n"), + ) +} + +async fn get_expense( + Path(id): Path, + State(shared_state): State>, +) -> impl IntoResponse { + let expense: Expense = sqlx::query_as("SELECT * FROM expenses WHERE id = $1") + .bind(id) + .fetch_one(&shared_state.pool) + .await + .unwrap(); + + Html(format!( + TABLE_ROW!(), + expense.description, expense.price, expense.expense_type, expense.is_essencial, expense.id + )) +} + +async fn update_expense( + Path(id): Path, + State(shared_state): State>, +) -> impl IntoResponse { + let expense: Expense = sqlx::query_as("SELECT * FROM expenses WHERE id = $1") + .bind(id) + .fetch_one(&shared_state.pool) + .await + .unwrap(); + + Html(format!( + TABLE_ROW!(), + expense.description, expense.price, expense.expense_type, expense.is_essencial, expense.id + )) +} diff --git a/templates/expenses.html b/templates/expenses.html index 6b2eb83..9ea20de 100644 --- a/templates/expenses.html +++ b/templates/expenses.html @@ -11,8 +11,11 @@

Carol's Expenses

- - + + + + + diff --git a/templates/hello.html b/templates/hello.html deleted file mode 100644 index abb5331..0000000 --- a/templates/hello.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Carol Expenses{% endblock %} - -{% block content %} - -
-

Carol Expenses

-Hello, {{ name }}! -
- -{% endblock %} From d3e59abe2c21ec3147cf847163b2075756d761f6 Mon Sep 17 00:00:00 2001 From: Nicolas Auler Date: Wed, 20 Dec 2023 18:17:51 -0300 Subject: [PATCH 2/2] Simplified readme --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index ae0729b..0593555 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,5 @@ # Contributing -## Create and activate a virtual environment to use pre-commit -```shell -virtualenv . -source bin/activate -``` - ## Install pre-commit ```shell curl -LO https://github.com/pre-commit/pre-commit/releases/download/v3.6.0/pre-commit-3.6.0.pyz
IdCostDescriptionPriceCategoryEssencial