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