feat: added event form & POST route
							parent
							
								
									3add93bdb2
								
							
						
					
					
						commit
						adef5c1fd5
					
				| 
						 | 
				
			
			@ -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<Utc>,
 | 
			
		||||
    date: NaiveDate,
 | 
			
		||||
    description: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Event {
 | 
			
		||||
    pub fn from_row(row: Row<'_>) -> rusqlite::Result<Self> {
 | 
			
		||||
    pub fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
 | 
			
		||||
        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<Utc>,
 | 
			
		||||
    date: NaiveDate,
 | 
			
		||||
    description: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NewEvent {
 | 
			
		||||
    pub fn insert(self, pool: &DbPool) -> Result<Event, DbError> {
 | 
			
		||||
        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,
 | 
			
		||||
        )?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<SqliteConnectionManager>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Vec<_>, _> = stmt.query_map((self.id,), Comment::from_row)?.collect();
 | 
			
		||||
        Ok(comments?)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn events(&self, pool: &DbPool) -> Result<Vec<Event>, DbError> {
 | 
			
		||||
        let conn = pool.get()?;
 | 
			
		||||
        let mut stmt = conn.prepare("select * from events where plant_id = $1")?;
 | 
			
		||||
 | 
			
		||||
        let events: Result<Vec<_>, _> = stmt.query_map((self.id,), Event::from_row)?.collect();
 | 
			
		||||
        Ok(events?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NewPlant {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<crate::Context> {
 | 
			
		||||
    Router::new().route("/", post(post_comment))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn post_comment(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<crate::Context> {
 | 
			
		||||
    Router::new().route("/", post(post_event))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn post_event(
 | 
			
		||||
    State(ctx): State<crate::Context>,
 | 
			
		||||
    Form(event): Form<db::NewEvent>,
 | 
			
		||||
) -> super::Result<Html<String>> {
 | 
			
		||||
    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)?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<crate::Context> {
 | 
			
		||||
    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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
{% macro li(event) %}
 | 
			
		||||
<li>
 | 
			
		||||
    <div class="event">
 | 
			
		||||
        <b>{{ event.event_type }}</b>
 | 
			
		||||
        <p>{{ event.date }}</p>
 | 
			
		||||
        <p>{{ event.description }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
</li>
 | 
			
		||||
{% endmacro li %}
 | 
			
		||||
 | 
			
		||||
{% macro form(plant_id, target="#events_ul") %}
 | 
			
		||||
<form hx-post="/events" hx-target="{{ target }}" hx-swap="beforeend">
 | 
			
		||||
    <input type="hidden" id="plant_id" name="plant_id" value="{{ plant_id }}">
 | 
			
		||||
    <label for="event_type">Type:</label>
 | 
			
		||||
    <select id="event_type" name="event_type">
 | 
			
		||||
        {% for type in event_types %}
 | 
			
		||||
        <option value="{{ type }}">{{ type }}</option>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </select>
 | 
			
		||||
    <label for="date">Date:</label>
 | 
			
		||||
    <input type="date" id="date" name="date">
 | 
			
		||||
    <label for="description">Description:</label>
 | 
			
		||||
    <textarea id="description" name="description" rows=2></textarea></br>
 | 
			
		||||
    <input type="submit">
 | 
			
		||||
</form>
 | 
			
		||||
{% endmacro form %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
{% import "macros/event.html" as macros_event %}
 | 
			
		||||
{{ macros_event::li(event=event) }}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,20 @@
 | 
			
		|||
{% import "macros/event.html" as macros_event %}
 | 
			
		||||
 | 
			
		||||
<h1>Calathea</h1>
 | 
			
		||||
<h2>{{ plant.name }}</h2>
 | 
			
		||||
<h3>Species</h3>
 | 
			
		||||
<p>{{ plant.species }}</p>
 | 
			
		||||
<h3>Description</h3>
 | 
			
		||||
<p>{{ plant.description }}</p>
 | 
			
		||||
<h3>Events</h3>
 | 
			
		||||
<div id="events">
 | 
			
		||||
    <ul id="events_ul">
 | 
			
		||||
        {% for event in events %}
 | 
			
		||||
            {{ macros_event::li(event=event) }}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </ul>
 | 
			
		||||
    {{ macros_event::form(plant_id=plant.id) }}
 | 
			
		||||
</div>
 | 
			
		||||
<h3>Comments</h3>
 | 
			
		||||
<ul id="comments">
 | 
			
		||||
{% for comment in comments %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,5 @@
 | 
			
		|||
{% import "macros/event.html" as macros_event %}
 | 
			
		||||
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue