From adef5c1fd5af06facefe27542c9f327b2c6e092f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 10 Jan 2025 13:00:36 +0100 Subject: [PATCH] feat: added event form & POST route --- src/db/event.rs | 34 ++++++++++++++++++++++---- src/db/mod.rs | 1 + src/db/plant.rs | 10 +++++++- src/main.rs | 8 ++++++ src/migrations/003_events.sql | 2 +- src/server/comments.rs | 4 +-- src/server/events.rs | 21 ++++++++++++++++ src/server/mod.rs | 8 +++--- src/server/plants.rs | 10 +++++--- src/templates/macros/event.html | 26 ++++++++++++++++++++ src/templates/partials/event_li.html | 2 ++ src/templates/partials/plant_info.html | 11 +++++++++ src/templates/plant_page.html | 2 ++ 13 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 src/server/events.rs create mode 100644 src/templates/macros/event.html create mode 100644 src/templates/partials/event_li.html diff --git a/src/db/event.rs b/src/db/event.rs index 08aa6f3..59148ec 100644 --- a/src/db/event.rs +++ b/src/db/event.rs @@ -1,6 +1,6 @@ use std::{fmt::Display, str::FromStr}; -use chrono::Utc; +use chrono::NaiveDate; use r2d2_sqlite::rusqlite::{ self, types::{FromSql, FromSqlError}, @@ -8,6 +8,10 @@ use r2d2_sqlite::rusqlite::{ }; use serde::{Deserialize, Serialize}; +use super::{DbError, DbPool}; + +pub const EVENT_TYPES: [&str; 1] = ["Watering"]; + #[derive(Serialize, Deserialize)] pub enum EventType { Watering, @@ -63,17 +67,17 @@ pub struct Event { id: i32, plant_id: i32, event_type: EventType, - time: chrono::DateTime, + date: NaiveDate, description: String, } impl Event { - pub fn from_row(row: Row<'_>) -> rusqlite::Result { + pub fn from_row(row: &Row<'_>) -> Result { Ok(Self { id: row.get("id")?, plant_id: row.get("plant_id")?, event_type: row.get("event_type")?, - time: row.get("time")?, + date: row.get("date")?, description: row.get("description")?, }) } @@ -83,6 +87,26 @@ impl Event { pub struct NewEvent { plant_id: i32, event_type: EventType, - time: chrono::DateTime, + date: NaiveDate, description: String, } + +impl NewEvent { + pub fn insert(self, pool: &DbPool) -> Result { + let conn = pool.get()?; + + let mut stmt = conn.prepare( + "insert into events (plant_id, event_type, date, description) values ($1, $2, $3, $4) returning *", + )?; + + Ok(stmt.query_row( + ( + &self.plant_id, + &self.event_type, + &self.date, + &self.description, + ), + Event::from_row, + )?) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 9af9c10..75ad3c0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -7,6 +7,7 @@ use r2d2_sqlite::{rusqlite, SqliteConnectionManager}; use std::{error::Error, fmt}; pub use comment::{Comment, NewComment}; +pub use event::{Event, EventType, NewEvent, EVENT_TYPES}; pub use plant::{NewPlant, Plant}; pub type DbPool = r2d2::Pool; diff --git a/src/db/plant.rs b/src/db/plant.rs index 21e3609..faeae78 100644 --- a/src/db/plant.rs +++ b/src/db/plant.rs @@ -1,7 +1,7 @@ use r2d2_sqlite::rusqlite::{self, Row}; use serde::{Deserialize, Serialize}; -use super::{Comment, DbError, DbPool}; +use super::{Comment, DbError, DbPool, Event}; #[derive(Serialize)] pub struct Plant { @@ -55,6 +55,14 @@ impl Plant { let comments: Result, _> = stmt.query_map((self.id,), Comment::from_row)?.collect(); Ok(comments?) } + + pub fn events(&self, pool: &DbPool) -> Result, DbError> { + let conn = pool.get()?; + let mut stmt = conn.prepare("select * from events where plant_id = $1")?; + + let events: Result, _> = stmt.query_map((self.id,), Event::from_row)?.collect(); + Ok(events?) + } } impl NewPlant { diff --git a/src/main.rs b/src/main.rs index 2119d53..da92aa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,14 @@ fn load_templates() -> Tera { "partials/comment_li.html", include_str!("templates/partials/comment_li.html"), ), + ( + "partials/event_li.html", + include_str!("templates/partials/event_li.html"), + ), + ( + "macros/event.html", + include_str!("templates/macros/event.html"), + ), ]) .unwrap(); diff --git a/src/migrations/003_events.sql b/src/migrations/003_events.sql index 2151bb7..2d17f79 100644 --- a/src/migrations/003_events.sql +++ b/src/migrations/003_events.sql @@ -4,6 +4,6 @@ create table events ( references plants (id) on delete cascade, event_type text not null, - time text not null, + date text not null, description text not null ); diff --git a/src/server/comments.rs b/src/server/comments.rs index ca9f889..c5f7f9d 100644 --- a/src/server/comments.rs +++ b/src/server/comments.rs @@ -3,8 +3,8 @@ use tera::Context; use crate::db::{Comment, NewComment}; -pub fn app(ctx: crate::Context) -> axum::Router { - Router::new().route("/", post(post_comment)).with_state(ctx) +pub fn app() -> axum::Router { + Router::new().route("/", post(post_comment)) } async fn post_comment( diff --git a/src/server/events.rs b/src/server/events.rs new file mode 100644 index 0000000..ed916f5 --- /dev/null +++ b/src/server/events.rs @@ -0,0 +1,21 @@ +use axum::{extract::State, response::Html, routing::post, Form, Router}; +use tera::Context; + +use crate::db; + +pub fn app() -> axum::Router { + Router::new().route("/", post(post_event)) +} + +async fn post_event( + State(ctx): State, + Form(event): Form, +) -> super::Result> { + let event = tokio::task::spawn_blocking(move || event.insert(&ctx.pool)) + .await + .unwrap()?; + + let mut context = Context::new(); + context.insert("event", &event); + Ok(Html(ctx.tera.render("partials/event_li.html", &context)?)) +} diff --git a/src/server/mod.rs b/src/server/mod.rs index de2ecb8..3879144 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,6 @@ mod comments; mod error; +mod events; mod plants; use axum::{ @@ -32,9 +33,10 @@ pub fn render_partial(headers: &HeaderMap) -> bool { pub fn app(ctx: crate::Context) -> axum::Router { let mut router = Router::new() .route("/", get(get_index)) - .with_state(ctx.clone()) - .nest("/plants", plants::app(ctx.clone())) - .nest("/comments", comments::app(ctx.clone())); + .nest("/plants", plants::app()) + .nest("/comments", comments::app()) + .nest("/events", events::app()) + .with_state(ctx.clone()); for (name, content) in crate::STATIC_FILES { router = router.route(&format!("/static/{}", name), get(content)) diff --git a/src/server/plants.rs b/src/server/plants.rs index 279d3e9..f736cc7 100644 --- a/src/server/plants.rs +++ b/src/server/plants.rs @@ -11,11 +11,10 @@ use crate::db::{self, DbError, Plant}; use super::{error::AppError, render_partial}; -pub fn app(ctx: crate::Context) -> axum::Router { +pub fn app() -> axum::Router { Router::new() .route("/:id", get(get_plant_page)) .route("/", post(post_plant)) - .with_state(ctx) } async fn get_plant_page( @@ -28,8 +27,9 @@ async fn get_plant_page( if let Some(plant) = plant { let comments = plant.comments(&ctx.pool)?; + let events = plant.events(&ctx.pool)?; - Ok::<_, DbError>(Some((plant, comments))) + Ok::<_, DbError>(Some((plant, comments, events))) } else { Ok(None) } @@ -38,10 +38,12 @@ async fn get_plant_page( .unwrap()?; match res { - Some((plant, comments)) => { + Some((plant, comments, events)) => { let mut context = Context::new(); context.insert("plant", &plant); context.insert("comments", &comments); + context.insert("events", &events); + context.insert("event_types", &db::EVENT_TYPES); let tmpl = if render_partial(&headers) { "partials/plant_info.html" diff --git a/src/templates/macros/event.html b/src/templates/macros/event.html new file mode 100644 index 0000000..a188c67 --- /dev/null +++ b/src/templates/macros/event.html @@ -0,0 +1,26 @@ +{% macro li(event) %} +
  • +
    + {{ event.event_type }} +

    {{ event.date }}

    +

    {{ event.description }}

    +
    +
  • +{% endmacro li %} + +{% macro form(plant_id, target="#events_ul") %} +
    + + + + + + +
    + +
    +{% endmacro form %} diff --git a/src/templates/partials/event_li.html b/src/templates/partials/event_li.html new file mode 100644 index 0000000..e72babc --- /dev/null +++ b/src/templates/partials/event_li.html @@ -0,0 +1,2 @@ +{% import "macros/event.html" as macros_event %} +{{ macros_event::li(event=event) }} diff --git a/src/templates/partials/plant_info.html b/src/templates/partials/plant_info.html index c42a65d..7f19f26 100644 --- a/src/templates/partials/plant_info.html +++ b/src/templates/partials/plant_info.html @@ -1,9 +1,20 @@ +{% import "macros/event.html" as macros_event %} +

    Calathea

    {{ plant.name }}

    Species

    {{ plant.species }}

    Description

    {{ plant.description }}

    +

    Events

    +
    +
      + {% for event in events %} + {{ macros_event::li(event=event) }} + {% endfor %} +
    + {{ macros_event::form(plant_id=plant.id) }} +

    Comments

      {% for comment in comments %} diff --git a/src/templates/plant_page.html b/src/templates/plant_page.html index 0fb41ef..cf2133a 100644 --- a/src/templates/plant_page.html +++ b/src/templates/plant_page.html @@ -1,3 +1,5 @@ +{% import "macros/event.html" as macros_event %} + {% extends "base.html" %} {% block content %}