feat: added event form & POST route

image-uploads
Jef Roosens 2025-01-10 13:00:36 +01:00
parent 3add93bdb2
commit adef5c1fd5
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
13 changed files with 123 additions and 16 deletions

View File

@ -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,
)?)
}
}

View File

@ -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>;

View File

@ -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 {

View File

@ -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();

View File

@ -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
);

View File

@ -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(

View File

@ -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)?))
}

View File

@ -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))

View File

@ -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"

View File

@ -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 %}

View File

@ -0,0 +1,2 @@
{% import "macros/event.html" as macros_event %}
{{ macros_event::li(event=event) }}

View File

@ -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 %}

View File

@ -1,3 +1,5 @@
{% import "macros/event.html" as macros_event %}
{% extends "base.html" %}
{% block content %}