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]]
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",

View File

@ -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

View File

@ -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
);

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" (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 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
}
}

View File

@ -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;

View File

@ -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,
});
}

View File

@ -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
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,
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,
}

View File

@ -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);
}
}

View File

@ -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")
}

View File

@ -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)) => {

View File

@ -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

View File

@ -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.",
),
}
}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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]">&#9888; {{ 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 %}&#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 }}"
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 %}