forked from Ruderverein-Donau-Linz/rowt
Merge commit 'e5088bddb0ab694abac5f6d2ab2d76a5042e52bb' into feature/frontend-triptype
# Conflicts: # templates/index.html.tera
This commit is contained in:
commit
15644e8a0b
37
Cargo.lock
generated
37
Cargo.lock
generated
@ -1238,9 +1238,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.4"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
|
||||
checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@ -1540,9 +1540,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.5.7"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122"
|
||||
checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
@ -1550,9 +1550,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.5.7"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15"
|
||||
checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
@ -1560,9 +1560,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.5.7"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e"
|
||||
checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
@ -1573,9 +1573,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.5.7"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e"
|
||||
checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
@ -1655,9 +1655,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
@ -1964,9 +1964,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.37.15"
|
||||
version = "0.37.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece"
|
||||
checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno",
|
||||
@ -2489,9 +2489,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.13"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cd2598a37719e3cd4c28af93f978506a97a2920ef4d96e4b12e38b8cbc8940"
|
||||
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@ -2529,10 +2529,11 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.38"
|
||||
version = "0.1.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d"
|
||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
|
20
README.md
20
README.md
@ -1,20 +1,26 @@
|
||||
# TODO
|
||||
- [ ] Allow sign-outs only >2h before event
|
||||
|
||||
# Icons
|
||||
- Regatta: 🏅
|
||||
- Lange Ausfahrt: 💪
|
||||
- Wanderfahrt: ⛱
|
||||
|
||||
# Notes / Bugfixes
|
||||
- [] max_people = 0 -> Rot hervorheben, dass Ausfahrt abgesagt wurde?
|
||||
- [] my trips for cox
|
||||
- [] add `trip_type` (id, name, desc, question, icon) with a FK to `trip_details`
|
||||
## Frontend
|
||||
- [] add UI for `trip_type`
|
||||
- [] 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)
|
||||
- [] exactly same time -> deny registration
|
||||
- [] `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
|
||||
## Frontend
|
||||
- [] my trips for cox
|
||||
|
||||
## Backend
|
||||
- [] exactly same time -> deny registration
|
||||
- [] automatically add regular planned trip
|
||||
- [] User sync w/ nextcloud
|
||||
- [] remove key from src/rest/admin/rss.rs (line 8); at least before putting code somewhere public
|
||||
|
@ -8,19 +8,29 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||
"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" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"planned_starting_time" text NOT NULL,
|
||||
"max_people" INTEGER 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" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL,
|
||||
"planned_amount_cox" INTEGER unsigned NOT NULL,
|
||||
"allow_guests" boolean NOT NULL default false,
|
||||
"trip_details_id" INTEGER NOT NULL,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
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,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
@ -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" (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?', '🏅')
|
||||
|
@ -3,22 +3,23 @@ use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use self::{
|
||||
planned_event::{PlannedEvent, PlannedEventWithUser},
|
||||
trip::{Trip, TripWithUser},
|
||||
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
};
|
||||
|
||||
pub mod log;
|
||||
pub mod planned_event;
|
||||
pub mod trip;
|
||||
pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
pub mod user;
|
||||
pub mod usertrip;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Day {
|
||||
day: NaiveDate,
|
||||
planned_events: Vec<PlannedEventWithUser>,
|
||||
trips: Vec<TripWithUser>,
|
||||
planned_events: Vec<PlannedEventWithUserAndTriptype>,
|
||||
trips: Vec<TripWithUserAndType>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
@ -29,4 +30,21 @@ impl Day {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,28 @@
|
||||
use chrono::NaiveDate;
|
||||
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 id: i64,
|
||||
name: String,
|
||||
planned_amount_cox: i64,
|
||||
allow_guests: bool,
|
||||
trip_details_id: i64,
|
||||
planned_starting_time: String,
|
||||
max_people: i64,
|
||||
day: String,
|
||||
notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PlannedEventWithUser {
|
||||
pub struct PlannedEventWithUserAndTriptype {
|
||||
#[serde(flatten)]
|
||||
planned_event: PlannedEvent,
|
||||
pub planned_event: PlannedEvent,
|
||||
trip_type: Option<TripType>,
|
||||
cox_needed: bool,
|
||||
cox: Vec<Registration>,
|
||||
rower: Vec<Registration>,
|
||||
@ -39,7 +41,8 @@ impl PlannedEvent {
|
||||
sqlx::query_as!(
|
||||
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
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
WHERE planned_event.id like ?
|
||||
@ -51,11 +54,14 @@ WHERE planned_event.id like ?
|
||||
.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 events = sqlx::query_as!(
|
||||
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
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
WHERE day=?",
|
||||
@ -68,17 +74,23 @@ WHERE day=?",
|
||||
let mut ret = Vec::new();
|
||||
for event in events {
|
||||
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(),
|
||||
cox_needed: event.planned_amount_cox > cox.len() as i64,
|
||||
cox,
|
||||
rower: event.get_all_rower(db).await,
|
||||
trip_type,
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
async fn get_all_cox(&self, db: &SqlitePool) -> Vec<Registration> {
|
||||
//TODO: switch to join
|
||||
sqlx::query_as!(
|
||||
Registration,
|
||||
"
|
||||
@ -96,6 +108,7 @@ FROM trip WHERE planned_event_id = ?
|
||||
}
|
||||
|
||||
async fn get_all_rower(&self, db: &SqlitePool) -> Vec<Registration> {
|
||||
//TODO: switch to join
|
||||
sqlx::query_as!(
|
||||
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
|
||||
}
|
||||
|
||||
//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(
|
||||
db: &SqlitePool,
|
||||
name: String,
|
||||
planned_amount_cox: i32,
|
||||
allow_guests: bool,
|
||||
trip_details: TripDetails,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO planned_event(name, planned_amount_cox, allow_guests, trip_details_id) VALUES(?, ?, ?, ?)",
|
||||
name, planned_amount_cox, allow_guests, trip_details.id
|
||||
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
||||
name,
|
||||
planned_amount_cox,
|
||||
trip_details.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -159,7 +190,7 @@ mod test {
|
||||
|
||||
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 =
|
||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
|
@ -5,6 +5,7 @@ use sqlx::SqlitePool;
|
||||
use super::{
|
||||
planned_event::{PlannedEvent, Registration},
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::CoxUser,
|
||||
};
|
||||
|
||||
@ -18,13 +19,16 @@ pub struct Trip {
|
||||
max_people: i64,
|
||||
day: String,
|
||||
notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TripWithUser {
|
||||
pub struct TripWithUserAndType {
|
||||
#[serde(flatten)]
|
||||
trip: Trip,
|
||||
pub trip: Trip,
|
||||
rower: Vec<Registration>,
|
||||
trip_type: Option<TripType>,
|
||||
}
|
||||
|
||||
impl Trip {
|
||||
@ -44,7 +48,7 @@ impl Trip {
|
||||
sqlx::query_as!(
|
||||
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
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
@ -63,19 +67,7 @@ WHERE trip.id=?
|
||||
cox: &CoxUser,
|
||||
planned_event: &PlannedEvent,
|
||||
) -> Result<(), CoxHelpError> {
|
||||
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 = ?",
|
||||
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 {
|
||||
if planned_event.is_rower_registered(db, &cox).await {
|
||||
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 trips = sqlx::query_as!(
|
||||
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
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
@ -108,10 +100,16 @@ WHERE day=?
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut ret = Vec::new();
|
||||
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_type,
|
||||
rower: trip.get_all_rower(db).await,
|
||||
});
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ pub struct TripDetails {
|
||||
max_people: i64,
|
||||
day: String,
|
||||
notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
}
|
||||
|
||||
impl TripDetails {
|
||||
@ -15,7 +17,7 @@ impl TripDetails {
|
||||
sqlx::query_as!(
|
||||
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
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -33,13 +35,17 @@ WHERE id like ?
|
||||
max_people: i32,
|
||||
day: String,
|
||||
notes: Option<String>,
|
||||
allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
) -> i64 {
|
||||
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,
|
||||
max_people,
|
||||
day,
|
||||
notes
|
||||
notes,
|
||||
allow_guests,
|
||||
trip_type_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -57,14 +63,7 @@ WHERE id like ?
|
||||
.unwrap(); //TODO: fixme
|
||||
let amount_currently_registered = i64::from(amount_currently_registered.count);
|
||||
|
||||
let amount_allowed_to_register =
|
||||
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
|
||||
amount_currently_registered >= self.max_people
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,11 +93,29 @@ mod test {
|
||||
let pool = testdb!();
|
||||
|
||||
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,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
@ -115,4 +132,6 @@ mod test {
|
||||
fn test_true_full() {
|
||||
//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
55
src/model/triptype.rs
Normal 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
|
||||
}
|
@ -16,8 +16,8 @@ pub struct User {
|
||||
pub name: String,
|
||||
pw: Option<String>,
|
||||
pub is_cox: bool,
|
||||
is_admin: bool,
|
||||
is_guest: bool,
|
||||
pub is_admin: bool,
|
||||
pub is_guest: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
deleted: bool,
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ impl UserTrip {
|
||||
return Err(UserTripError::EventAlreadyFull);
|
||||
}
|
||||
|
||||
if user.is_guest && !trip_details.allow_guests {
|
||||
return Err(UserTripError::GuestNotAllowedForThisEvent);
|
||||
}
|
||||
|
||||
//check if cox if own event
|
||||
let is_cox = sqlx::query!(
|
||||
"SELECT count(*) as amount
|
||||
@ -30,6 +34,7 @@ impl UserTrip {
|
||||
return Err(UserTripError::CantRegisterAtOwnEvent);
|
||||
}
|
||||
|
||||
//TODO: can probably move to trip.rs?
|
||||
//check if cox if planned_event
|
||||
let is_cox = sqlx::query!(
|
||||
"SELECT count(*) as amount
|
||||
@ -80,6 +85,7 @@ pub enum UserTripError {
|
||||
AlreadyRegisteredAsCox,
|
||||
EventAlreadyFull,
|
||||
CantRegisterAtOwnEvent,
|
||||
GuestNotAllowedForThisEvent,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -180,4 +186,19 @@ mod test {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ struct AddPlannedEventForm {
|
||||
planned_starting_time: String,
|
||||
max_people: i32,
|
||||
notes: Option<String>,
|
||||
trip_type: Option<i64>,
|
||||
}
|
||||
|
||||
#[post("/planned-event", data = "<data>")]
|
||||
@ -33,6 +34,8 @@ async fn create(
|
||||
data.max_people,
|
||||
data.day.clone(),
|
||||
data.notes.clone(),
|
||||
data.allow_guests,
|
||||
data.trip_type,
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -41,14 +44,7 @@ async fn create(
|
||||
//the object
|
||||
|
||||
//TODO: fix clone()
|
||||
PlannedEvent::create(
|
||||
db,
|
||||
data.name.clone(),
|
||||
data.planned_amount_cox,
|
||||
data.allow_guests,
|
||||
trip_details,
|
||||
)
|
||||
.await;
|
||||
PlannedEvent::create(db, data.name.clone(), data.planned_amount_cox, trip_details).await;
|
||||
|
||||
Flash::success(Redirect::to("/"), "Successfully planned the event")
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ async fn login(
|
||||
) -> Flash<Redirect> {
|
||||
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 {
|
||||
Ok(user) => user,
|
||||
Err(LoginError::NoPasswordSet(user)) => {
|
||||
|
@ -14,13 +14,16 @@ use crate::model::{
|
||||
user::CoxUser,
|
||||
};
|
||||
|
||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||
#[derive(FromForm)]
|
||||
struct AddTripForm {
|
||||
day: String,
|
||||
//TODO: properly parse `planned_starting_time`
|
||||
planned_starting_time: String,
|
||||
#[field(validate = range(1..))]
|
||||
max_people: i32,
|
||||
notes: Option<String>,
|
||||
trip_type: Option<i64>,
|
||||
allow_guests: bool,
|
||||
}
|
||||
|
||||
#[post("/trip", data = "<data>")]
|
||||
@ -32,6 +35,8 @@ async fn create(db: &State<SqlitePool>, data: Form<AddTripForm>, cox: CoxUser) -
|
||||
data.max_people,
|
||||
data.day.clone(),
|
||||
data.notes.clone(),
|
||||
data.allow_guests,
|
||||
data.trip_type,
|
||||
)
|
||||
.await;
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
|
||||
|
@ -13,6 +13,7 @@ use sqlx::SqlitePool;
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::User,
|
||||
usertrip::{UserTrip, UserTripError},
|
||||
Day,
|
||||
@ -22,25 +23,39 @@ mod admin;
|
||||
mod auth;
|
||||
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("/")]
|
||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let mut days = Vec::new();
|
||||
|
||||
let mut show_next_n_days = 6;
|
||||
if user.is_cox {
|
||||
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap();
|
||||
show_next_n_days = end_of_year
|
||||
.signed_duration_since(Local::now().date_naive())
|
||||
.num_days()
|
||||
+ 1;
|
||||
let mut context = Context::new();
|
||||
|
||||
if user.is_cox || user.is_admin {
|
||||
let triptypes = TripType::all(db).await;
|
||||
context.insert("trip_types", &triptypes);
|
||||
}
|
||||
|
||||
let show_next_n_days = amount_days_to_show(user.is_cox);
|
||||
for i in 0..show_next_n_days {
|
||||
let date = (Local::now() + Duration::days(i)).date_naive();
|
||||
|
||||
if user.is_guest {
|
||||
days.push(Day::new_guest(db, date).await);
|
||||
} else {
|
||||
days.push(Day::new(db, date).await);
|
||||
}
|
||||
|
||||
let mut context = Context::new();
|
||||
}
|
||||
|
||||
if let Some(msg) = flash {
|
||||
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("/"),
|
||||
"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.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,10 +50,13 @@
|
||||
{% 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>
|
||||
{% 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>
|
||||
<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"/>
|
||||
<div class="grid gap-3">
|
||||
<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>
|
||||
</form>
|
||||
{% endfor %}
|
||||
|
@ -14,10 +14,10 @@
|
||||
<input type="hidden" name="remember" value="true">
|
||||
<div class="-space-y-px rounded-md shadow-sm">
|
||||
<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>
|
||||
{{ 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>
|
||||
|
||||
|
@ -9,10 +9,10 @@
|
||||
<input type="hidden" name="userid" value="{{ userid }}" />
|
||||
<div class="-space-y-px rounded-md shadow-sm">
|
||||
<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>
|
||||
{{ 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>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
{{ 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 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') }}
|
||||
<select name="trip_type">
|
||||
<option selected value>Reguläre Ausfahrt</option>
|
||||
|
@ -5,6 +5,7 @@
|
||||
<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='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') }}
|
||||
<select name="trip_type">
|
||||
<option selected value>Reguläre Ausfahrt</option>
|
||||
|
@ -24,9 +24,11 @@
|
||||
<div class="h-8"></div>
|
||||
{% endmacro header %}
|
||||
|
||||
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='') %}
|
||||
<label for="{{ name }}" class="sr-only">{{ label }}</label>
|
||||
<input id="{{ name }}" name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{{ label }}" {% if min %} min="{{ min }}" {% endif %}>
|
||||
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false) %}
|
||||
<div>
|
||||
<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 %}
|
||||
|
||||
{% macro checkbox(label, name, id='', checked=false) %}
|
||||
@ -41,7 +43,7 @@
|
||||
</div>
|
||||
{% 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="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md">
|
||||
{% if participants | length > 0 %}
|
||||
|
@ -142,13 +142,19 @@
|
||||
<div class="pt-2 reset-js" data-coxneeded="false">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<strong class="text-primary-900">{{ trip.planned_starting_time }} Uhr</strong> <small
|
||||
class="text-gray-600">({{ trip.cox_name }})</small><br />
|
||||
{% if trip.max_people == 0 %}
|
||||
<strong class="text-[#f43f5e]">⚠ {{ trip.planned_starting_time }} Uhr</strong>
|
||||
<small class="text-[#f43f5e]">(Absage {{ trip.cox_name }})</small>
|
||||
{% else %}
|
||||
<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"
|
||||
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 %}⚠ {% 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 }}"
|
||||
class="inline-block link-primary mr-3">
|
||||
Details
|
||||
@ -177,8 +183,14 @@
|
||||
{# --- START Sidebar Content --- #}
|
||||
<div class="hidden">
|
||||
<div id="trip{{ trip.trip_details_id }}">
|
||||
{% if trip.max_people == 0 %}
|
||||
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
|
||||
{{ 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 %}
|
||||
<div class="bg-gray-100 p-3 mt-4 rounded-md">
|
||||
@ -246,7 +258,13 @@
|
||||
</div>
|
||||
|
||||
{% include "dynamics/sidebar" %}
|
||||
|
||||
{% if loggedin_user.is_cox %}
|
||||
{% include "forms/trip" %}
|
||||
{% endif %}
|
||||
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% include "forms/event" %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock content %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user