Merge commit 'e5088bddb0ab694abac5f6d2ab2d76a5042e52bb' into feature/frontend-triptype

# Conflicts:
#	templates/index.html.tera
This commit is contained in:
Marie Birner 2023-05-03 15:26:41 +02:00
commit 15644e8a0b
22 changed files with 326 additions and 122 deletions

37
Cargo.lock generated
View File

@ -1238,9 +1238,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.4" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -1540,9 +1540,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.5.7" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122" checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
dependencies = [ dependencies = [
"thiserror", "thiserror",
"ucd-trie", "ucd-trie",
@ -1550,9 +1550,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_derive" name = "pest_derive"
version = "2.5.7" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15" checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
dependencies = [ dependencies = [
"pest", "pest",
"pest_generator", "pest_generator",
@ -1560,9 +1560,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_generator" name = "pest_generator"
version = "2.5.7" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e" checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
dependencies = [ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
@ -1573,9 +1573,9 @@ dependencies = [
[[package]] [[package]]
name = "pest_meta" name = "pest_meta"
version = "2.5.7" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e" checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"pest", "pest",
@ -1655,9 +1655,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.26" version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]] [[package]]
name = "polyval" name = "polyval"
@ -1964,9 +1964,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.15" version = "0.37.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"errno", "errno",
@ -2489,9 +2489,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.13" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cd2598a37719e3cd4c28af93f978506a97a2920ef4d96e4b12e38b8cbc8940" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@ -2529,10 +2529,11 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.38" version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [ dependencies = [
"cfg-if",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",

View File

@ -1,20 +1,26 @@
# TODO
- [ ] Allow sign-outs only >2h before event
# Icons # Icons
- Regatta: 🏅 - Regatta: 🏅
- Lange Ausfahrt: 💪 - Lange Ausfahrt: 💪
- Wanderfahrt: ⛱ - Wanderfahrt: ⛱
# Notes / Bugfixes # Notes / Bugfixes
- [] max_people = 0 -> Rot hervorheben, dass Ausfahrt abgesagt wurde? ## Frontend
- [] my trips for cox - [] add UI for `trip_type`
- [] add `trip_type` (id, name, desc, question, icon) with a FK to `trip_details` - [] support esc to close sidebar
- [] FAQ page
## Backend
- [] Allow sign-outs only >2h before event
- [] add `always_show` to `planned_trips` (e.g. for wanderfahrten) - [] add `always_show` to `planned_trips` (e.g. for wanderfahrten)
- [] exactly same time -> deny registration
- [] `planned_events` -> ICS feed to be [integrated](https://icscalendar.com/shortcode-overview/) in homepage calendar - [] `planned_events` -> ICS feed to be [integrated](https://icscalendar.com/shortcode-overview/) in homepage calendar
- [] Notification system (-> signal msgs?) e.g. if new event; somebody unregistered
# Nice to have # Nice to have
## Frontend
- [] my trips for cox
## Backend
- [] exactly same time -> deny registration
- [] automatically add regular planned trip - [] automatically add regular planned trip
- [] User sync w/ nextcloud - [] User sync w/ nextcloud
- [] remove key from src/rest/admin/rss.rs (line 8); at least before putting code somewhere public - [] remove key from src/rest/admin/rss.rs (line 8); at least before putting code somewhere public

View File

@ -8,19 +8,29 @@ CREATE TABLE IF NOT EXISTS "user" (
"deleted" boolean NOT NULL DEFAULT FALSE "deleted" boolean NOT NULL DEFAULT FALSE
); );
CREATE TABLE IF NOT EXISTS "trip_type" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE,
"desc" text NOT NULL,
"question" text NOT NULL,
"icon" text NOT NULL
);
CREATE TABLE IF NOT EXISTS "trip_details" ( CREATE TABLE IF NOT EXISTS "trip_details" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"planned_starting_time" text NOT NULL, "planned_starting_time" text NOT NULL,
"max_people" INTEGER NOT NULL, "max_people" INTEGER NOT NULL,
"day" TEXT NOT NULL, "day" TEXT NOT NULL,
"notes" TEXT "allow_guests" boolean NOT NULL default false,
"notes" TEXT,
"trip_type_id" INTEGER,
FOREIGN KEY(trip_type_id) REFERENCES trip_type(id)
); );
CREATE TABLE IF NOT EXISTS "planned_event" ( CREATE TABLE IF NOT EXISTS "planned_event" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL, "name" text NOT NULL,
"planned_amount_cox" INTEGER unsigned NOT NULL, "planned_amount_cox" INTEGER unsigned NOT NULL,
"allow_guests" boolean NOT NULL default false,
"trip_details_id" INTEGER NOT NULL, "trip_details_id" INTEGER NOT NULL,
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(trip_details_id) REFERENCES trip_details(id) ON DELETE CASCADE FOREIGN KEY(trip_details_id) REFERENCES trip_details(id) ON DELETE CASCADE
@ -52,3 +62,4 @@ CREATE TABLE IF NOT EXISTS "log" (
"msg" text NOT NULL, "msg" text NOT NULL,
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP
); );

View File

@ -12,3 +12,4 @@ INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2); INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅')

View File

@ -3,22 +3,23 @@ use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use self::{ use self::{
planned_event::{PlannedEvent, PlannedEventWithUser}, planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
trip::{Trip, TripWithUser}, trip::{Trip, TripWithUserAndType},
}; };
pub mod log; pub mod log;
pub mod planned_event; pub mod planned_event;
pub mod trip; pub mod trip;
pub mod tripdetails; pub mod tripdetails;
pub mod triptype;
pub mod user; pub mod user;
pub mod usertrip; pub mod usertrip;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Day { pub struct Day {
day: NaiveDate, day: NaiveDate,
planned_events: Vec<PlannedEventWithUser>, planned_events: Vec<PlannedEventWithUserAndTriptype>,
trips: Vec<TripWithUser>, trips: Vec<TripWithUserAndType>,
} }
impl Day { impl Day {
@ -29,4 +30,21 @@ impl Day {
trips: Trip::get_for_day(db, day).await, trips: Trip::get_for_day(db, day).await,
} }
} }
pub async fn new_guest(db: &SqlitePool, day: NaiveDate) -> Self {
let mut day = Self::new(db, day).await;
day.planned_events = day
.planned_events
.into_iter()
.filter(|e| e.planned_event.allow_guests)
.collect();
day.trips = day
.trips
.into_iter()
.filter(|t| t.trip.allow_guests)
.collect();
day
}
} }

View File

@ -1,26 +1,28 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::{FromRow, SqlitePool};
use super::tripdetails::TripDetails; use super::{tripdetails::TripDetails, triptype::TripType, user::User};
#[derive(Serialize, Clone)] #[derive(Serialize, Clone, FromRow)]
pub struct PlannedEvent { pub struct PlannedEvent {
pub id: i64, pub id: i64,
name: String, name: String,
planned_amount_cox: i64, planned_amount_cox: i64,
allow_guests: bool,
trip_details_id: i64, trip_details_id: i64,
planned_starting_time: String, planned_starting_time: String,
max_people: i64, max_people: i64,
day: String, day: String,
notes: Option<String>, notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct PlannedEventWithUser { pub struct PlannedEventWithUserAndTriptype {
#[serde(flatten)] #[serde(flatten)]
planned_event: PlannedEvent, pub planned_event: PlannedEvent,
trip_type: Option<TripType>,
cox_needed: bool, cox_needed: bool,
cox: Vec<Registration>, cox: Vec<Registration>,
rower: Vec<Registration>, rower: Vec<Registration>,
@ -39,7 +41,8 @@ impl PlannedEvent {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT planned_event.id, name, planned_amount_cox, allow_guests, trip_details_id, planned_starting_time, max_people, day, notes SELECT
planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id
FROM planned_event FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE planned_event.id like ? WHERE planned_event.id like ?
@ -51,11 +54,14 @@ WHERE planned_event.id like ?
.ok() .ok()
} }
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<PlannedEventWithUser> { pub async fn get_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<PlannedEventWithUserAndTriptype> {
let day = format!("{day}"); let day = format!("{day}");
let events = sqlx::query_as!( let events = sqlx::query_as!(
PlannedEvent, PlannedEvent,
"SELECT planned_event.id, name, planned_amount_cox, allow_guests, trip_details_id, planned_starting_time, max_people, day, notes "SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id
FROM planned_event FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE day=?", WHERE day=?",
@ -68,17 +74,23 @@ WHERE day=?",
let mut ret = Vec::new(); let mut ret = Vec::new();
for event in events { for event in events {
let cox = event.get_all_cox(db).await; let cox = event.get_all_cox(db).await;
ret.push(PlannedEventWithUser { let mut trip_type = None;
if let Some(trip_type_id) = event.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await;
}
ret.push(PlannedEventWithUserAndTriptype {
planned_event: event.clone(), planned_event: event.clone(),
cox_needed: event.planned_amount_cox > cox.len() as i64, cox_needed: event.planned_amount_cox > cox.len() as i64,
cox, cox,
rower: event.get_all_rower(db).await, rower: event.get_all_rower(db).await,
trip_type,
}); });
} }
ret ret
} }
async fn get_all_cox(&self, db: &SqlitePool) -> Vec<Registration> { async fn get_all_cox(&self, db: &SqlitePool) -> Vec<Registration> {
//TODO: switch to join
sqlx::query_as!( sqlx::query_as!(
Registration, Registration,
" "
@ -96,6 +108,7 @@ FROM trip WHERE planned_event_id = ?
} }
async fn get_all_rower(&self, db: &SqlitePool) -> Vec<Registration> { async fn get_all_rower(&self, db: &SqlitePool) -> Vec<Registration> {
//TODO: switch to join
sqlx::query_as!( sqlx::query_as!(
Registration, Registration,
" "
@ -112,16 +125,34 @@ FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM planned_even
.unwrap() //Okay, as PlannedEvent can only be created with proper DB backing .unwrap() //Okay, as PlannedEvent can only be created with proper DB backing
} }
//TODO: add tests
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!(
"SELECT count(*) as amount
FROM user_trip
WHERE trip_details_id =
(SELECT trip_details_id FROM planned_event WHERE id = ?)
AND user_id = ?",
self.id,
user.id
)
.fetch_one(db)
.await
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
is_rower.amount > 0
}
pub async fn create( pub async fn create(
db: &SqlitePool, db: &SqlitePool,
name: String, name: String,
planned_amount_cox: i32, planned_amount_cox: i32,
allow_guests: bool,
trip_details: TripDetails, trip_details: TripDetails,
) { ) {
sqlx::query!( sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, allow_guests, trip_details_id) VALUES(?, ?, ?, ?)", "INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
name, planned_amount_cox, allow_guests, trip_details.id name,
planned_amount_cox,
trip_details.id
) )
.execute(db) .execute(db)
.await .await
@ -159,7 +190,7 @@ mod test {
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
PlannedEvent::create(&pool, "new-event".into(), 2, false, trip_details).await; PlannedEvent::create(&pool, "new-event".into(), 2, trip_details).await;
let res = let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;

View File

@ -5,6 +5,7 @@ use sqlx::SqlitePool;
use super::{ use super::{
planned_event::{PlannedEvent, Registration}, planned_event::{PlannedEvent, Registration},
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType,
user::CoxUser, user::CoxUser,
}; };
@ -18,13 +19,16 @@ pub struct Trip {
max_people: i64, max_people: i64,
day: String, day: String,
notes: Option<String>, notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct TripWithUser { pub struct TripWithUserAndType {
#[serde(flatten)] #[serde(flatten)]
trip: Trip, pub trip: Trip,
rower: Vec<Registration>, rower: Vec<Registration>,
trip_type: Option<TripType>,
} }
impl Trip { impl Trip {
@ -44,7 +48,7 @@ impl Trip {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id
FROM trip FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id INNER JOIN user ON trip.cox_id = user.id
@ -63,19 +67,7 @@ WHERE trip.id=?
cox: &CoxUser, cox: &CoxUser,
planned_event: &PlannedEvent, planned_event: &PlannedEvent,
) -> Result<(), CoxHelpError> { ) -> Result<(), CoxHelpError> {
let is_rower = sqlx::query!( if planned_event.is_rower_registered(db, &cox).await {
"SELECT count(*) as amount
FROM user_trip
WHERE trip_details_id =
(SELECT trip_details_id FROM planned_event WHERE id = ?)
AND user_id = ?",
planned_event.id,
cox.id
)
.fetch_one(db)
.await
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
if is_rower.amount > 0 {
return Err(CoxHelpError::AlreadyRegisteredAsRower); return Err(CoxHelpError::AlreadyRegisteredAsRower);
} }
@ -92,12 +84,12 @@ WHERE trip.id=?
} }
} }
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUser> { pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
let day = format!("{day}"); let day = format!("{day}");
let trips = sqlx::query_as!( let trips = sqlx::query_as!(
Trip, Trip,
" "
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id
FROM trip FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id INNER JOIN user ON trip.cox_id = user.id
@ -108,10 +100,16 @@ WHERE day=?
.fetch_all(db) .fetch_all(db)
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
let mut ret = Vec::new(); let mut ret = Vec::new();
for trip in trips { for trip in trips {
ret.push(TripWithUser { let mut trip_type = None;
if let Some(trip_type_id) = trip.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await;
}
ret.push(TripWithUserAndType {
trip: trip.clone(), trip: trip.clone(),
trip_type,
rower: trip.get_all_rower(db).await, rower: trip.get_all_rower(db).await,
}); });
} }

View File

@ -8,6 +8,8 @@ pub struct TripDetails {
max_people: i64, max_people: i64,
day: String, day: String,
notes: Option<String>, notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
} }
impl TripDetails { impl TripDetails {
@ -15,7 +17,7 @@ impl TripDetails {
sqlx::query_as!( sqlx::query_as!(
TripDetails, TripDetails,
" "
SELECT id, planned_starting_time, max_people, day, notes SELECT id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id
FROM trip_details FROM trip_details
WHERE id like ? WHERE id like ?
", ",
@ -33,13 +35,17 @@ WHERE id like ?
max_people: i32, max_people: i32,
day: String, day: String,
notes: Option<String>, notes: Option<String>,
allow_guests: bool,
trip_type_id: Option<i64>,
) -> i64 { ) -> i64 {
let query = sqlx::query!( let query = sqlx::query!(
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes) VALUES(?, ?, ?, ?)" , "INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id) VALUES(?, ?, ?, ?, ?, ?)" ,
planned_starting_time, planned_starting_time,
max_people, max_people,
day, day,
notes notes,
allow_guests,
trip_type_id
) )
.execute(db) .execute(db)
.await .await
@ -57,14 +63,7 @@ WHERE id like ?
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
let amount_currently_registered = i64::from(amount_currently_registered.count); let amount_currently_registered = i64::from(amount_currently_registered.count);
let amount_allowed_to_register = amount_currently_registered >= self.max_people
sqlx::query!("SELECT max_people FROM trip_details WHERE id = ?", self.id)
.fetch_one(db)
.await
.unwrap(); //Okay, TripDetails can only be created if self.id exists
let amount_allowed_to_register = amount_allowed_to_register.max_people;
amount_currently_registered >= amount_allowed_to_register
} }
} }
@ -94,11 +93,29 @@ mod test {
let pool = testdb!(); let pool = testdb!();
assert_eq!( assert_eq!(
TripDetails::create(&pool, "10:00".into(), 2, "1970-01-01".into(), None).await, TripDetails::create(
&pool,
"10:00".into(),
2,
"1970-01-01".into(),
None,
false,
None
)
.await,
3, 3,
); );
assert_eq!( assert_eq!(
TripDetails::create(&pool, "10:00".into(), 2, "1970-01-01".into(), None).await, TripDetails::create(
&pool,
"10:00".into(),
2,
"1970-01-01".into(),
None,
false,
None
)
.await,
4, 4,
); );
} }
@ -115,4 +132,6 @@ mod test {
fn test_true_full() { fn test_true_full() {
//TODO: register user for trip_details = 1; check if is_full returns true //TODO: register user for trip_details = 1; check if is_full returns true
} }
//TODO: add new tripdetails test with trip_type != None
} }

55
src/model/triptype.rs Normal file
View File

@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct TripType {
pub id: i64,
name: String,
desc: String,
question: String,
icon: String,
}
impl TripType {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, desc, question, icon
FROM trip_type
WHERE id like ?
",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, desc, question, icon
FROM trip_type
"
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
}
#[cfg(test)]
mod test {
use crate::testdb;
use sqlx::SqlitePool;
#[sqlx::test]
fn test_find_true() {
let pool = testdb!();
}
//TODO: write tests
}

View File

@ -16,8 +16,8 @@ pub struct User {
pub name: String, pub name: String,
pw: Option<String>, pw: Option<String>,
pub is_cox: bool, pub is_cox: bool,
is_admin: bool, pub is_admin: bool,
is_guest: bool, pub is_guest: bool,
#[serde(default = "bool::default")] #[serde(default = "bool::default")]
deleted: bool, deleted: bool,
} }

View File

@ -14,6 +14,10 @@ impl UserTrip {
return Err(UserTripError::EventAlreadyFull); return Err(UserTripError::EventAlreadyFull);
} }
if user.is_guest && !trip_details.allow_guests {
return Err(UserTripError::GuestNotAllowedForThisEvent);
}
//check if cox if own event //check if cox if own event
let is_cox = sqlx::query!( let is_cox = sqlx::query!(
"SELECT count(*) as amount "SELECT count(*) as amount
@ -30,6 +34,7 @@ impl UserTrip {
return Err(UserTripError::CantRegisterAtOwnEvent); return Err(UserTripError::CantRegisterAtOwnEvent);
} }
//TODO: can probably move to trip.rs?
//check if cox if planned_event //check if cox if planned_event
let is_cox = sqlx::query!( let is_cox = sqlx::query!(
"SELECT count(*) as amount "SELECT count(*) as amount
@ -80,6 +85,7 @@ pub enum UserTripError {
AlreadyRegisteredAsCox, AlreadyRegisteredAsCox,
EventAlreadyFull, EventAlreadyFull,
CantRegisterAtOwnEvent, CantRegisterAtOwnEvent,
GuestNotAllowedForThisEvent,
} }
#[cfg(test)] #[cfg(test)]
@ -180,4 +186,19 @@ mod test {
assert_eq!(result, UserTripError::AlreadyRegisteredAsCox); assert_eq!(result, UserTripError::AlreadyRegisteredAsCox);
} }
#[sqlx::test]
fn test_fail_create_guest() {
let pool = testdb!();
let user = User::find_by_name(&pool, "guest".into()).await.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
let result = UserTrip::create(&pool, &user, &trip_details)
.await
.expect_err("Not allowed for guests");
assert_eq!(result, UserTripError::GuestNotAllowedForThisEvent);
}
} }

View File

@ -18,6 +18,7 @@ struct AddPlannedEventForm {
planned_starting_time: String, planned_starting_time: String,
max_people: i32, max_people: i32,
notes: Option<String>, notes: Option<String>,
trip_type: Option<i64>,
} }
#[post("/planned-event", data = "<data>")] #[post("/planned-event", data = "<data>")]
@ -33,6 +34,8 @@ async fn create(
data.max_people, data.max_people,
data.day.clone(), data.day.clone(),
data.notes.clone(), data.notes.clone(),
data.allow_guests,
data.trip_type,
) )
.await; .await;
@ -41,14 +44,7 @@ async fn create(
//the object //the object
//TODO: fix clone() //TODO: fix clone()
PlannedEvent::create( PlannedEvent::create(db, data.name.clone(), data.planned_amount_cox, trip_details).await;
db,
data.name.clone(),
data.planned_amount_cox,
data.allow_guests,
trip_details,
)
.await;
Flash::success(Redirect::to("/"), "Successfully planned the event") Flash::success(Redirect::to("/"), "Successfully planned the event")
} }

View File

@ -41,7 +41,6 @@ async fn login(
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let user = User::login(db, login.name.clone(), login.password.clone()).await; let user = User::login(db, login.name.clone(), login.password.clone()).await;
//TODO: be able to use ? for login. This would get rid of the following match clause.
let user = match user { let user = match user {
Ok(user) => user, Ok(user) => user,
Err(LoginError::NoPasswordSet(user)) => { Err(LoginError::NoPasswordSet(user)) => {

View File

@ -14,13 +14,16 @@ use crate::model::{
user::CoxUser, user::CoxUser,
}; };
//TODO: add constraints (e.g. planned_amount_cox > 0)
#[derive(FromForm)] #[derive(FromForm)]
struct AddTripForm { struct AddTripForm {
day: String, day: String,
//TODO: properly parse `planned_starting_time`
planned_starting_time: String, planned_starting_time: String,
#[field(validate = range(1..))]
max_people: i32, max_people: i32,
notes: Option<String>, notes: Option<String>,
trip_type: Option<i64>,
allow_guests: bool,
} }
#[post("/trip", data = "<data>")] #[post("/trip", data = "<data>")]
@ -32,6 +35,8 @@ async fn create(db: &State<SqlitePool>, data: Form<AddTripForm>, cox: CoxUser) -
data.max_people, data.max_people,
data.day.clone(), data.day.clone(),
data.notes.clone(), data.notes.clone(),
data.allow_guests,
data.trip_type,
) )
.await; .await;
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just

View File

@ -13,6 +13,7 @@ use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
log::Log, log::Log,
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType,
user::User, user::User,
usertrip::{UserTrip, UserTripError}, usertrip::{UserTrip, UserTripError},
Day, Day,
@ -22,25 +23,39 @@ mod admin;
mod auth; mod auth;
mod cox; mod cox;
fn amount_days_to_show(is_cox: bool) -> i64 {
if is_cox {
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap();
end_of_year
.signed_duration_since(Local::now().date_naive())
.num_days()
+ 1
} else {
6
}
}
#[get("/")] #[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template { async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut days = Vec::new(); let mut days = Vec::new();
let mut show_next_n_days = 6; let mut context = Context::new();
if user.is_cox {
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); if user.is_cox || user.is_admin {
show_next_n_days = end_of_year let triptypes = TripType::all(db).await;
.signed_duration_since(Local::now().date_naive()) context.insert("trip_types", &triptypes);
.num_days()
+ 1;
} }
let show_next_n_days = amount_days_to_show(user.is_cox);
for i in 0..show_next_n_days { for i in 0..show_next_n_days {
let date = (Local::now() + Duration::days(i)).date_naive(); let date = (Local::now() + Duration::days(i)).date_naive();
days.push(Day::new(db, date).await);
}
let mut context = Context::new(); if user.is_guest {
days.push(Day::new_guest(db, date).await);
} else {
days.push(Day::new(db, date).await);
}
}
if let Some(msg) = flash { if let Some(msg) = flash {
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
@ -81,6 +96,10 @@ async fn join(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash
Redirect::to("/"), Redirect::to("/"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
), ),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
),
} }
} }

View File

@ -50,10 +50,13 @@
{% if user.pw %} {% if user.pw %}
<a class="inline-block mt-1 text-primary-600 hover:text-primary-900 underline" href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a> <a class="inline-block mt-1 text-primary-600 hover:text-primary-900 underline" href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
{% endif %} {% endif %}
<a class="inline-block mt-1 text-primary-600 hover:text-primary-900 underline" href="/admin/user/{{ user.id }}/delete" onclick="return confirm('Really delete user?');">User löschen</a>
</div> </div>
<div> <div class="grid gap-3">
<input value="Ändern" type="submit" class="w-28 rounded-md bg-primary-600 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"/> <a href="/admin/user/{{ user.id }}/delete" class="inline-block btn btn-alert" onclick="return confirm('Wirklich löschen?');">
{% include "includes/delete-icon" %}
Löschen
</a>
<input value="Ändern" type="submit" class="w-28 btn btn-primary"/>
</div> </div>
</form> </form>
{% endfor %} {% endfor %}

View File

@ -14,10 +14,10 @@
<input type="hidden" name="remember" value="true"> <input type="hidden" name="remember" value="true">
<div class="-space-y-px rounded-md shadow-sm"> <div class="-space-y-px rounded-md shadow-sm">
<div> <div>
{{ macros::input(label='Name', name='name', type='input', required=true, class='rounded-t-md') }} {{ macros::input(label='Name', name='name', type='input', required=true, class='rounded-t-md',hide_label=true) }}
</div> </div>
<div> <div>
{{ macros::input(label='Passwort', name='password', type='password', class='rounded-b-md') }} {{ macros::input(label='Passwort', name='password', type='password', class='rounded-b-md',hide_label=true) }}
</div> </div>
</div> </div>

View File

@ -9,10 +9,10 @@
<input type="hidden" name="userid" value="{{ userid }}" /> <input type="hidden" name="userid" value="{{ userid }}" />
<div class="-space-y-px rounded-md shadow-sm"> <div class="-space-y-px rounded-md shadow-sm">
<div> <div>
{{ macros::input(label='Passwort', name='password', type='password', required=true, class='rounded-t-md') }} {{ macros::input(label='Passwort', name='password', type='password', required=true, class='rounded-t-md',hide_label=true) }}
</div> </div>
<div> <div>
{{ macros::input(label='Passwort bestätigen', name='password_confirm', type='password', required=true, class='rounded-b-md') }} {{ macros::input(label='Passwort bestätigen', name='password_confirm', type='password', required=true, class='rounded-b-md',hide_label=true) }}
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@
{{ macros::input(label='Startzeit', name='planned_starting_time', type='time', required=true) }} {{ macros::input(label='Startzeit', name='planned_starting_time', type='time', required=true) }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }} {{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }}
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }} {{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
{{ macros::checkbox(label='Gäste erlauben', name='max_allow_guestspeople') }} {{ macros::checkbox(label='Gäste erlauben', name='allow_guests') }}
{{ macros::input(label='Anmerkungen', name='notes', type='input') }} {{ macros::input(label='Anmerkungen', name='notes', type='input') }}
<select name="trip_type"> <select name="trip_type">
<option selected value>Reguläre Ausfahrt</option> <option selected value>Reguläre Ausfahrt</option>

View File

@ -5,6 +5,7 @@
<input class="day-js" type="hidden" name="day" value="" /> <input class="day-js" type="hidden" name="day" value="" />
{{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }} {{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }}
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }} {{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
{{ macros::checkbox(label='Gäste erlauben', name='allow_guests') }}
{{ macros::input(label='Anmerkungen', name='notes', type='input') }} {{ macros::input(label='Anmerkungen', name='notes', type='input') }}
<select name="trip_type"> <select name="trip_type">
<option selected value>Reguläre Ausfahrt</option> <option selected value>Reguläre Ausfahrt</option>

View File

@ -24,9 +24,11 @@
<div class="h-8"></div> <div class="h-8"></div>
{% endmacro header %} {% endmacro header %}
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='') %} {% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false) %}
<label for="{{ name }}" class="sr-only">{{ label }}</label> <div>
<input id="{{ name }}" name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{{ label }}" {% if min %} min="{{ min }}" {% endif %}> <label for="{{ name }}" class="{% if hide_label %} sr-only {% else %} small text-gray-600 {% endif %}">{{ label }}</label>
<input id="{{ name }}" name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min %} min="{{ min }}" {% endif %}>
</div>
{% endmacro input %} {% endmacro input %}
{% macro checkbox(label, name, id='', checked=false) %} {% macro checkbox(label, name, id='', checked=false) %}
@ -41,7 +43,7 @@
</div> </div>
{% endmacro alert %} {% endmacro alert %}
{% macro box(participants, empty_seats, header='Freie Plätze:', text='Keine Ruderer angemeldet', bg='primary-600', color='white') %} {% macro box(participants, empty_seats='', header='Freie Plätze:', text='Keine Ruderer angemeldet', bg='primary-600', color='white') %}
<div class="text-{{ color }} bg-{{ bg }} text-center p-1 mt-1 rounded-t-md">{{ header }} {{ empty_seats }}</div> <div class="text-{{ color }} bg-{{ bg }} text-center p-1 mt-1 rounded-t-md">{{ header }} {{ empty_seats }}</div>
<div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md"> <div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md">
{% if participants | length > 0 %} {% if participants | length > 0 %}

View File

@ -142,13 +142,19 @@
<div class="pt-2 reset-js" data-coxneeded="false"> <div class="pt-2 reset-js" data-coxneeded="false">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<strong class="text-primary-900">{{ trip.planned_starting_time }} Uhr</strong> <small {% if trip.max_people == 0 %}
class="text-gray-600">({{ trip.cox_name }})</small><br /> <strong class="text-[#f43f5e]">&#9888; {{ trip.planned_starting_time }} Uhr</strong>
{% if trip.trip_type %} <small class="text-[#f43f5e]">(Absage {{ trip.cox_name }})</small>
Spezielles Event: {{ trip.trip_type.name }} {% else %}
{% endif %} <strong class="text-primary-900">{{ trip.planned_starting_time }} Uhr</strong>
<small class="text-gray-600">({{ trip.cox_name }})</small>
{% endif %}
<br />
{% if trip.trip_type %}
Spezielles Event: {{ trip.trip_type.name }}
{% endif %}
<a href="#" data-sidebar="true" data-trigger="sidebar" <a href="#" data-sidebar="true" data-trigger="sidebar"
data-header="<strong>{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-header="<strong>{% if trip.max_people == 0 %}&#9888; {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}"
data-body="#trip{{ trip.trip_details_id }}" data-body="#trip{{ trip.trip_details_id }}"
class="inline-block link-primary mr-3"> class="inline-block link-primary mr-3">
Details Details
@ -177,9 +183,15 @@
{# --- START Sidebar Content --- #} {# --- START Sidebar Content --- #}
<div class="hidden"> <div class="hidden">
<div id="trip{{ trip.trip_details_id }}"> <div id="trip{{ trip.trip_details_id }}">
{% set amount_cur_rower = trip.rower | length %} {% if trip.max_people == 0 %}
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black') }} {# --- border-[#f43f5e] bg-[#f43f5e] --- #}
{# --- START Edit Form --- #} {{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
{% else %}
{% set amount_cur_rower = trip.rower | length %}
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black') }}
{% endif %}
{# --- START Edit Form --- #}
{% if trip.cox_id == loggedin_user.id %} {% if trip.cox_id == loggedin_user.id %}
<div class="bg-gray-100 p-3 mt-4 rounded-md"> <div class="bg-gray-100 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3> <h3 class="text-primary-950 font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
@ -246,7 +258,13 @@
</div> </div>
{% include "dynamics/sidebar" %} {% include "dynamics/sidebar" %}
{% include "forms/trip" %}
{% include "forms/event" %} {% if loggedin_user.is_cox %}
{% include "forms/trip" %}
{% endif %}
{% if loggedin_user.is_admin %}
{% include "forms/event" %}
{% endif %}
{% endblock content %} {% endblock content %}