From 72abdf118205e731018066d17735515020ad5606 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Mon, 7 Apr 2025 20:33:55 +0200 Subject: [PATCH] work on routes --- migration.sql | 4 +- src/lib.rs | 5 ++ src/route.rs | 201 ++++++++++++++++++++++++++++++++++++++++++ src/station/mod.rs | 2 +- src/station/routes.rs | 10 +-- 5 files changed, 211 insertions(+), 11 deletions(-) create mode 100644 src/route.rs diff --git a/migration.sql b/migration.sql index 1b23dd8..911de24 100644 --- a/migration.sql +++ b/migration.sql @@ -11,7 +11,7 @@ CREATE TABLE station ( CREATE TABLE route ( id INTEGER PRIMARY KEY, - name TEXT NOT NULL -- e.g. 'wiwö' + name TEXT NOT NULL UNIQUE -- e.g. 'wiwö' ); CREATE TABLE route_station ( @@ -29,8 +29,8 @@ CREATE TABLE "group" ( notes TEXT, amount_people INTEGER, first_station_id INTEGER NOT NULL, - FOREIGN KEY (first_station_id) REFERENCES station(id) route_id INTEGER NOT NULL, + FOREIGN KEY (first_station_id) REFERENCES station(id), FOREIGN KEY (route_id) REFERENCES route(id) ); diff --git a/src/lib.rs b/src/lib.rs index 323c627..3c2f475 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use tokio::net::TcpListener; use tower_sessions::{MemoryStore, Session, SessionManagerLayer}; mod partials; +mod route; mod station; #[macro_export] @@ -81,6 +82,9 @@ async fn index(session: Session) -> Markup { a role="button" href="/station" { "Stationen" } + a role="button" href="/route" { + "Routen" + } }; page(content, session, false).await } @@ -93,6 +97,7 @@ pub async fn start(listener: TcpListener, db: SqlitePool) { let app = Router::new() .route("/", get(index)) .nest("/station", station::routes()) + .nest("/route", route::routes()) .route("/pico.css", get(serve_pico_css)) .route("/style.css", get(serve_my_css)) .route("/leaflet.css", get(serve_leaflet_css)) diff --git a/src/route.rs b/src/route.rs new file mode 100644 index 0000000..e030fa0 --- /dev/null +++ b/src/route.rs @@ -0,0 +1,201 @@ +use crate::{err, page, succ}; +use axum::{ + extract::State, + response::{IntoResponse, Redirect}, + routing::{get, post}, + Form, Router, +}; +use maud::{html, Markup}; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; +use std::sync::Arc; +use tower_sessions::Session; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +struct Route { + id: i64, + name: String, +} + +impl Route { + async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as::<_, Self>("SELECT id, name FROM route;") + .fetch_all(db) + .await + .unwrap() + } + + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { + sqlx::query_as!(Self, "SELECT id, name FROM route WHERE id = ?", id) + .fetch_one(db) + .await + .ok() + } + + async fn create(db: &SqlitePool, name: &str) -> Result<(), String> { + sqlx::query!("INSERT INTO route(name) VALUES (?)", name) + .execute(db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + async fn update_name(&self, db: &SqlitePool, name: &str) { + sqlx::query!("UPDATE route SET name = ? WHERE id = ?", name, self.id) + .execute(db) + .await + .unwrap(); + } + + async fn delete(&self, db: &SqlitePool) { + sqlx::query!("DELETE FROM route WHERE id = ?", self.id) + .execute(db) + .await + .unwrap(); + } +} + +async fn index(State(db): State>, session: Session) -> Markup { + let routes = Route::all(&db).await; + + let content = html! { + h1 { + a href="/" { "↩️" } + "Routen" + } + ol { + @for route in &routes{ + li { + a href=(format!("/route/{}", route.id)){ + (route.name) + } + a href=(format!("/route/{}/delete", route.id)) + onclick="return confirm('Bist du sicher, dass die Route gelöscht werden soll? Das kann _NICHT_ mehr rückgängig gemacht werden.');" { + "🗑️" + } + } + } + } + h2 { "Neue Route" } + form action="/route" method="post" { + fieldset role="group" { + input type="text" name="name" placeholder="Routenname" required; + input type="submit" value="Neue Route"; + } + } + }; + page(content, session, false).await +} + +#[derive(Deserialize)] +struct CreateForm { + name: String, +} + +async fn create( + State(db): State>, + session: Session, + Form(form): Form, +) -> impl IntoResponse { + match Route::create(&db, &form.name).await { + Ok(_) => succ!(session,"Route '{}' erfolgreich erstellt!", form.name), + Err(e) => err!( + session, + "Route '{}' konnte _NICHT_ erstellt werden, da es bereits eine Route mit diesem Namen gibt ({e})!", + form.name + ), + } + + Redirect::to("/route") +} + +async fn delete( + State(db): State>, + session: Session, + axum::extract::Path(id): axum::extract::Path, +) -> impl IntoResponse { + let Some(route) = Route::find_by_id(&db, id).await else { + err!( + session, + "Route mit ID {id} konnte nicht gelöscht werden, da sie nicht existiert" + ); + + return Redirect::to("/route"); + }; + + route.delete(&db).await; + + succ!(session, "Route '{}' erfolgreich gelöscht!", route.name); + + Redirect::to("/route") +} + +async fn view( + State(db): State>, + session: Session, + axum::extract::Path(id): axum::extract::Path, +) -> Result { + let Some(route) = Route::find_by_id(&db, id).await else { + err!( + session, + "Route mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert" + ); + + return Err(Redirect::to("/route")); + }; + + let content = html! { + h1 { + a href="/route" { "↩️" } + "Route " (route.name) + } + details { + summary { "Routenname bearbeiten ✏️" } + form action=(format!("/route/{}/name", route.id)) method="post" { + input type="text" name="name" value=(route.name) required; + input type="submit" value="Speichern"; + } + + } + }; + Ok(page(content, session, false).await) +} + +#[derive(Deserialize)] +struct UpdateNameForm { + name: String, +} +async fn update_name( + State(db): State>, + session: Session, + axum::extract::Path(id): axum::extract::Path, + Form(form): Form, +) -> impl IntoResponse { + let Some(route) = Route::find_by_id(&db, id).await else { + err!( + session, + "Route mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert" + ); + + return Redirect::to("/route"); + }; + + route.update_name(&db, &form.name).await; + + succ!( + session, + "Name der Route '{}' wurden erfolgreich bearbeitet!", + route.name + ); + + Redirect::to(&format!("/route/{id}")) +} + +pub(super) fn routes() -> Router> { + Router::new() + .route("/", get(index)) + .route("/", post(create)) + .route("/{id}/delete", get(delete)) + .route("/{id}", get(view)) + .route("/{id}/name", post(update_name)) +} diff --git a/src/station/mod.rs b/src/station/mod.rs index 17dc977..b74e20b 100644 --- a/src/station/mod.rs +++ b/src/station/mod.rs @@ -31,7 +31,7 @@ impl Station { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!( Self, - "SELECT id, name, notes, amount_people, last_login, pw, lat, lng FROM station WHERE id like ?", + "SELECT id, name, notes, amount_people, last_login, pw, lat, lng FROM station WHERE id = ?", id ) .fetch_one(db) diff --git a/src/station/routes.rs b/src/station/routes.rs index 9d637b3..6256fc6 100644 --- a/src/station/routes.rs +++ b/src/station/routes.rs @@ -22,13 +22,7 @@ async fn create( Form(form): Form, ) -> impl IntoResponse { match Station::create(&db, &form.name).await { - Ok(_) => session - .insert( - "succ", - &format!("Station '{}' erfolgreich erstellt!", form.name), - ) - .await - .unwrap(), + Ok(_) => succ!(session,"Station '{}' erfolgreich erstellt!", form.name), Err(e) => err!( session, "Station '{}' konnte _NICHT_ erstellt werden, da es bereits eine Station mit diesem Namen gibt ({e})!", @@ -402,6 +396,7 @@ async fn index(State(db): State>, session: Session) -> Markup { pub(super) fn routes() -> Router> { Router::new() .route("/", get(index)) + .route("/", post(create)) .route("/{id}", get(view)) .route("/{id}/delete", get(delete)) .route("/{id}/notes", post(update_notes)) @@ -409,5 +404,4 @@ pub(super) fn routes() -> Router> { .route("/{id}/amount-people-reset", get(update_amount_people_reset)) .route("/{id}/location", post(update_location)) .route("/{id}/location-clear", get(update_location_clear)) - .route("/", post(create)) }