From 3deb1e40fc78e263cc3c3eb81b30ce99b118c374 Mon Sep 17 00:00:00 2001 From: philipp Date: Fri, 8 Mar 2024 13:13:20 +0100 Subject: [PATCH] boatshouse functionality, fixes #183 --- migration.sql | 10 ++++ src/model/boat.rs | 35 +++++++++++ src/model/boathouse.rs | 90 +++++++++++++++++++++++++++++ src/model/mod.rs | 1 + src/tera/board/boathouse.rs | 85 +++++++++++++++++++++++++++ src/tera/board/mod.rs | 9 +++ src/tera/mod.rs | 2 + staging-diff.sql | 9 +++ templates/board/boathouse.html.tera | 48 +++++++++++++++ templates/index.html.tera | 3 + 10 files changed, 292 insertions(+) create mode 100644 src/model/boathouse.rs create mode 100644 src/tera/board/boathouse.rs create mode 100644 src/tera/board/mod.rs create mode 100644 templates/board/boathouse.html.tera diff --git a/migration.sql b/migration.sql index 2bbf1c9..2df76ef 100644 --- a/migration.sql +++ b/migration.sql @@ -140,3 +140,13 @@ CREATE TABLE IF NOT EXISTS "boat_damage" ( "verified_at" datetime, "lock_boat" boolean not null default false -- if true: noone can use the boat ); + +CREATE TABLE IF NOT EXISTS "boathouse" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "boat_id" INTEGER NOT NULL REFERENCES boat(id), + "aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')), + "side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')), + "level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 3), + CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space +); + diff --git a/src/model/boat.rs b/src/model/boat.rs index d40a50a..4354de8 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -181,6 +181,41 @@ ORDER BY amount_seats DESC Self::boats_to_details(db, boats).await } + pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec { + let boats = sqlx::query_as!( + Boat, + " +SELECT + b.id, + b.name, + b.amount_seats, + b.location_id, + b.owner, + b.year_built, + b.boatbuilder, + b.default_shipmaster_only_steering, + b.default_destination, + b.skull, + b.external +FROM + boat AS b +LEFT JOIN + boathouse AS bh ON b.id = bh.boat_id +WHERE + b.external = false + AND b.location_id = (SELECT id FROM location WHERE name = 'Linz') + AND bh.id IS NULL -- This ensures the boat does not have an entry in the boathouse table +ORDER BY + b.name DESC; + " + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + Self::boats_to_details(db, boats).await + } + pub async fn for_user(db: &SqlitePool, user: &User) -> Vec { if user.has_role(db, "admin").await { return Self::all(db).await; diff --git a/src/model/boathouse.rs b/src/model/boathouse.rs new file mode 100644 index 0000000..a3ae022 --- /dev/null +++ b/src/model/boathouse.rs @@ -0,0 +1,90 @@ +use std::collections::HashMap; + +use rocket::serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +use crate::tera::board::boathouse::FormBoathouseToAdd; + +use super::boat::Boat; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +pub struct Boathouse { + pub id: i64, + pub boat_id: i64, + pub aisle: String, + pub side: String, + pub level: i64, +} + +impl Boathouse { + pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 4]>> { + let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 4]>> = HashMap::new(); + + let mut mountain = HashMap::new(); + mountain.insert("mountain", [None, None, None, None]); + mountain.insert("water", [None, None, None, None]); + ret.insert("mountain-aisle", mountain); + + let mut middle = HashMap::new(); + middle.insert("mountain", [None, None, None, None]); + middle.insert("water", [None, None, None, None]); + ret.insert("middle-aisle", middle); + + let mut water = HashMap::new(); + water.insert("mountain", [None, None, None, None]); + water.insert("water", [None, None, None, None]); + ret.insert("water-aisle", water); + + let boathouses = sqlx::query_as!( + Boathouse, + "SELECT id, boat_id, aisle, side, level FROM boathouse" + ) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + + for boathouse in boathouses { + let aisle = ret + .get_mut(format!("{}-aisle", boathouse.aisle).as_str()) + .unwrap(); + let side = aisle.get_mut(boathouse.side.as_str()).unwrap(); + + side[boathouse.level as usize] = Some(( + boathouse.id, + Boat::find_by_id(db, boathouse.boat_id as i32) + .await + .unwrap(), + )); + } + + ret + } + + pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> { + sqlx::query!( + "INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)", + data.boat_id, + data.aisle, + data.side, + data.level + ) + .execute(db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { + sqlx::query_as!(Self, "SELECT * FROM boathouse WHERE id like ?", id) + .fetch_one(db) + .await + .ok() + } + + pub async fn delete(&self, db: &SqlitePool) { + sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id) + .execute(db) + .await + .unwrap(); //Okay, because we can only create a Boat of a valid id + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 44f1df9..6423379 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -9,6 +9,7 @@ use self::{ pub mod boat; pub mod boatdamage; +pub mod boathouse; pub mod family; pub mod location; pub mod log; diff --git a/src/tera/board/boathouse.rs b/src/tera/board/boathouse.rs new file mode 100644 index 0000000..94119ba --- /dev/null +++ b/src/tera/board/boathouse.rs @@ -0,0 +1,85 @@ +use crate::model::{ + boat::Boat, + boathouse::Boathouse, + user::{AdminUser, UserWithRoles, VorstandUser}, +}; +use rocket::{ + form::Form, + get, post, + request::FlashMessage, + response::{Flash, Redirect}, + routes, FromForm, Route, State, +}; +use rocket_dyn_templates::{tera::Context, Template}; +use sqlx::SqlitePool; + +#[get("/boathouse")] +async fn index( + db: &State, + admin: VorstandUser, + flash: Option>, +) -> Template { + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + + let boats = Boat::all_for_boatshouse(db).await; + context.insert("boats", &boats); + + let boathouse = Boathouse::get(db).await; + context.insert("boathouse", &boathouse); + + context.insert( + "loggedin_user", + &UserWithRoles::from_user(admin.into(), db).await, + ); + + Template::render("board/boathouse", context.into_json()) +} + +#[derive(FromForm)] +pub struct FormBoathouseToAdd { + pub boat_id: i32, + pub aisle: String, + pub side: String, + pub level: i32, +} +#[post("/boathouse", data = "")] +async fn new<'r>( + db: &State, + data: Form, + _admin: AdminUser, +) -> Flash { + match Boathouse::create(db, data.into_inner()).await { + Ok(_) => Flash::success(Redirect::to("/board/boathouse"), "Boot hinzugefügt"), + Err(e) => Flash::error(Redirect::to("/board/boathouse"), e), + } +} + +#[get("/boathouse//delete")] +async fn delete(db: &State, _admin: AdminUser, boathouse_id: i32) -> Flash { + let boat = Boathouse::find_by_id(db, boathouse_id).await; + match boat { + Some(boat) => { + boat.delete(db).await; + Flash::success(Redirect::to("/board/boathouse"), "Bootsplatz gelöscht") + } + None => Flash::error(Redirect::to("/board/boathouse"), "Boatplace does not exist"), + } +} +//#[post("/boat/new", data = "")] +//async fn create( +// db: &State, +// data: Form>, +// _admin: AdminUser, +//) -> Flash { +// match Boat::create(db, data.into_inner()).await { +// Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"), +// Err(e) => Flash::error(Redirect::to("/admin/boat"), e), +// } +//} + +pub fn routes() -> Vec { + routes![index, new, delete] +} diff --git a/src/tera/board/mod.rs b/src/tera/board/mod.rs new file mode 100644 index 0000000..4800fb8 --- /dev/null +++ b/src/tera/board/mod.rs @@ -0,0 +1,9 @@ +use rocket::Route; + +pub mod boathouse; + +pub fn routes() -> Vec { + let mut ret = Vec::new(); + ret.append(&mut boathouse::routes()); + ret +} diff --git a/src/tera/mod.rs b/src/tera/mod.rs index f4ec85a..ae52b07 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -21,6 +21,7 @@ use crate::model::user::{User, UserWithRoles}; pub(crate) mod admin; mod auth; +pub(crate) mod board; mod boatdamage; mod cox; mod ergo; @@ -89,6 +90,7 @@ pub fn config(rocket: Rocket) -> Rocket { .mount("/boatdamage", boatdamage::routes()) .mount("/cox", cox::routes()) .mount("/admin", admin::routes()) + .mount("/board", board::routes()) .mount("/", misc::routes()) .mount("/public", FileServer::from("static/")) .register("/", catchers![unauthorized_error, forbidden_error]) diff --git a/staging-diff.sql b/staging-diff.sql index e69de29..fe85d50 100644 --- a/staging-diff.sql +++ b/staging-diff.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS "boathouse" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "boat_id" INTEGER NOT NULL REFERENCES boat(id), + "aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')), + "side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')), + "level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 3), + CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space +); + diff --git a/templates/board/boathouse.html.tera b/templates/board/boathouse.html.tera new file mode 100644 index 0000000..c70cb17 --- /dev/null +++ b/templates/board/boathouse.html.tera @@ -0,0 +1,48 @@ +{% import "includes/macros" as macros %} +{% import "includes/forms/log" as log %} +{% import "includes/forms/boat" as boat %} +{% extends "base" %} +{% macro show_place(aisle_name, side_name, level) %} +
  • + {% set aisle = aisle_name ~ "-aisle" %} + {% set place = boathouse[aisle][side_name] %} + {% if place[level] %} + {{ place[level].1.name }} X + {% else %} +
    + {{ macros::select(label="Boot", data=boats, name="boat_id", id="boat_id", display=["name", " (","amount_seats", " x)"], wrapper_class="col-span-4") }} + + + + +
    + {% endif %} +
  • +{% endmacro show_place %} +{% macro show_side(aisle_name, side_name) %} +
    +
      + {{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 0) }} + {{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 1) }} + {{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 2) }} + {{ self::show_place(aisle_name = aisle_name, side_name = side_name, level = 3) }} +
    +
    +{% endmacro show_side %} +{% macro show_aisle(name) %} +
    + {{ self::show_side(aisle_name = name, side_name = "mountain") }} + {{ self::show_side(aisle_name = name, side_name = "water") }} +
    +{% endmacro show_aisle %} +{% block content %} + {% if flash %}{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}{% endif %} +
    +

    Bootshaus

    + {{ self::show_aisle(name = "mountain") }} + {{ self::show_aisle(name = "middle") }} + {{ self::show_aisle(name = "water") }} +
    +{% endblock content %} diff --git a/templates/index.html.tera b/templates/index.html.tera index 06c03e7..95f7932 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -110,6 +110,9 @@
  • User
  • +
  • + Bootshaus +