fix-ci #528

Merged
philipp merged 2 commits from fix-ci into staging 2024-05-22 00:24:34 +02:00
16 changed files with 386 additions and 192 deletions
Showing only changes of commit daf9460bf7 - Show all commits

View File

@ -118,7 +118,7 @@ pub enum LogbookCreateError {
BoatLocked, BoatLocked,
BoatNotFound, BoatNotFound,
TooManyRowers(usize, usize), TooManyRowers(usize, usize),
RowerAlreadyOnWater(User), RowerAlreadyOnWater(Box<User>),
RowerCreateError(i64, String), RowerCreateError(i64, String),
ArrivalNotAfterDeparture, ArrivalNotAfterDeparture,
SteeringPersonNotInRowers, SteeringPersonNotInRowers,
@ -386,7 +386,7 @@ ORDER BY departure DESC
let user = User::find_by_id(db, *rower as i32).await.unwrap(); let user = User::find_by_id(db, *rower as i32).await.unwrap();
if user.on_water(db).await { if user.on_water(db).await {
return Err(LogbookCreateError::RowerAlreadyOnWater(user)); return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
} }
} }

View File

@ -152,12 +152,27 @@ ORDER BY read_at DESC, created_at DESC;
} }
} }
} }
// Cox read notification about cancelled event
let re = Regex::new(r"^remove_trip_by_planned_event:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i32>() {
let _ = sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
self.user_id,
number
)
.execute(db)
.await
.unwrap();
}
}
}
} }
} }
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) { pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
sqlx::query!( sqlx::query!(
"DELETE FROM notification WHERE action_after_reading=? and read_at = null", "DELETE FROM notification WHERE action_after_reading=? and read_at is null",
action action
) )
.execute(db) .execute(db)

View File

@ -8,7 +8,7 @@ use ics::{
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, SqlitePool};
use super::{tripdetails::TripDetails, triptype::TripType, user::User}; use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)] #[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct PlannedEvent { pub struct PlannedEvent {
@ -17,7 +17,7 @@ pub struct PlannedEvent {
planned_amount_cox: i64, planned_amount_cox: i64,
trip_details_id: i64, trip_details_id: i64,
pub planned_starting_time: String, pub planned_starting_time: String,
max_people: i64, pub(crate) max_people: i64,
pub day: String, pub day: String,
pub notes: Option<String>, pub notes: Option<String>,
pub allow_guests: bool, pub allow_guests: bool,
@ -98,6 +98,15 @@ FROM trip WHERE planned_event_id = ?
} }
} }
pub struct EventUpdate<'a> {
pub name: &'a str,
pub planned_amount_cox: i32,
pub max_people: i32,
pub notes: Option<&'a str>,
pub always_show: bool,
pub is_locked: bool,
}
impl PlannedEvent { impl PlannedEvent {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
@ -207,50 +216,109 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
} }
//TODO: create unit test //TODO: create unit test
pub async fn update( pub async fn update(&self, db: &SqlitePool, update: &EventUpdate<'_>) {
&self,
db: &SqlitePool,
name: &str,
planned_amount_cox: i32,
max_people: i32,
notes: Option<&str>,
always_show: bool,
is_locked: bool,
) {
sqlx::query!( sqlx::query!(
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?", "UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
name, update.name,
planned_amount_cox, update.planned_amount_cox,
self.id self.id
) )
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing .unwrap(); //Okay, as planned_event can only be created with proper DB backing
let tripdetails = self.trip_details(db).await;
let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!( sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?", "UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
max_people, update.max_people,
notes, update.notes,
always_show, update.always_show,
is_locked, update.is_locked,
self.trip_details_id self.trip_details_id
) )
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing .unwrap(); //Okay, as planned_event can only be created with proper DB backing
if max_people == 0 && !was_already_cancelled {
let coxes = Registration::all_cox(db, self.id).await;
for user in coxes {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match notes {
Some(n) if !n.is_empty() => n,
_ => ".",
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt{}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!("remove_trip_by_planned_event:{}", self.id)),
)
.await;
}
}
let rower = Registration::all_rower(db, self.trip_details_id).await;
for user in rower {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match notes {
Some(n) if !n.is_empty() => n,
_ => ".",
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt{}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!(
"remove_user_trip_with_trip_details_id:{}",
tripdetails.id
)),
)
.await;
}
}
}
if max_people > 0 && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
)
.await;
Notification::delete_by_action(
db,
&format!("remove_trip_by_planned_event:{}", self.id),
)
.await;
}
} }
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> { pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
if Registration::all_rower(db, self.trip_details_id) if !Registration::all_rower(db, self.trip_details_id)
.await .await
.len() .is_empty()
> 0
{ {
return Err( return Err(
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(), "Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
); );
} }
if Registration::all_cox(db, self.trip_details_id).await.len() > 0 { if !Registration::all_cox(db, self.trip_details_id)
.await
.is_empty()
{
return Err( return Err(
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist." "Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
.into(), .into(),
@ -326,7 +394,7 @@ mod test {
let pool = testdb!(); let pool = testdb!();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap(); let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
planned_event.delete(&pool).await; planned_event.delete(&pool).await.unwrap();
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

@ -34,6 +34,16 @@ pub struct TripWithUserAndType {
trip_type: Option<TripType>, trip_type: Option<TripType>,
} }
pub struct TripUpdate<'a> {
pub cox: &'a CoxUser,
pub trip: &'a Trip,
pub max_people: i32,
pub notes: Option<&'a str>,
pub trip_type: Option<i64>, //TODO: Move to `TripType`
pub always_show: bool,
pub is_locked: bool,
}
impl TripWithUserAndType { impl TripWithUserAndType {
pub async fn from(db: &SqlitePool, trip: Trip) -> Self { pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
let mut trip_type = None; let mut trip_type = None;
@ -137,6 +147,10 @@ WHERE trip.id=?
return Err(CoxHelpError::DetailsLocked); return Err(CoxHelpError::DetailsLocked);
} }
if planned_event.max_people == 0 {
return Err(CoxHelpError::CanceledEvent);
}
match sqlx::query!( match sqlx::query!(
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)", "INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
cox.id, cox.id,
@ -177,40 +191,39 @@ WHERE day=?
/// Cox decides to update own trip. /// Cox decides to update own trip.
pub async fn update_own( pub async fn update_own(
db: &SqlitePool, db: &SqlitePool,
cox: &CoxUser, update: &TripUpdate<'_>,
trip: &Trip,
max_people: i32,
notes: Option<&str>,
trip_type: Option<i64>, //TODO: Move to `TripType`
always_show: bool,
is_locked: bool,
) -> Result<(), TripUpdateError> { ) -> Result<(), TripUpdateError> {
if !trip.is_trip_from_user(cox.id) { if !update.trip.is_trip_from_user(update.cox.id) {
return Err(TripUpdateError::NotYourTrip); return Err(TripUpdateError::NotYourTrip);
} }
let Some(trip_details_id) = trip.trip_details_id else { let Some(trip_details_id) = update.trip.trip_details_id else {
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
}; };
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!( sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?", "UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
max_people, update.max_people,
notes, update.notes,
trip_type, update.trip_type,
always_show, update.always_show,
is_locked, update.is_locked,
trip_details_id trip_details_id
) )
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, as trip_details can only be created with proper DB backing .unwrap(); //Okay, as trip_details can only be created with proper DB backing
if max_people == 0 { if update.max_people == 0 && !was_already_cancelled {
let rowers = TripWithUserAndType::from(db, trip.clone()).await.rower; let rowers = TripWithUserAndType::from(db, update.trip.clone())
.await
.rower;
for user in rowers { for user in rowers {
if let Some(user) = User::find_by_name(db, &user.name).await { if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match notes { let notes = match update.notes {
Some(n) if !n.is_empty() => n, Some(n) if !n.is_empty() => n,
_ => ".", _ => ".",
}; };
@ -220,7 +233,10 @@ WHERE day=?
&user, &user,
&format!( &format!(
"Die Ausfahrt von {} am {} um {} wurde abgesagt{}", "Die Ausfahrt von {} am {} um {} wurde abgesagt{}",
cox.user.name, trip.day, trip.planned_starting_time, notes update.cox.user.name,
update.trip.day,
update.trip.planned_starting_time,
notes
), ),
"Absage Ausfahrt", "Absage Ausfahrt",
None, None,
@ -240,6 +256,14 @@ WHERE day=?
.await; .await;
} }
if update.max_people > 0 && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
)
.await;
}
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
trip_details.check_free_spaces(db).await; trip_details.check_free_spaces(db).await;
@ -324,6 +348,7 @@ pub enum CoxHelpError {
AlreadyRegisteredAsRower, AlreadyRegisteredAsRower,
AlreadyRegisteredAsCox, AlreadyRegisteredAsCox,
DetailsLocked, DetailsLocked,
CanceledEvent,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -349,7 +374,7 @@ mod test {
use crate::{ use crate::{
model::{ model::{
planned_event::PlannedEvent, planned_event::PlannedEvent,
trip::TripDeleteError, trip::{self, TripDeleteError},
tripdetails::TripDetails, tripdetails::TripDetails,
user::{CoxUser, User}, user::{CoxUser, User},
usertrip::UserTrip, usertrip::UserTrip,
@ -434,11 +459,17 @@ mod test {
let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert!( let update = trip::TripUpdate {
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false) cox: &cox,
.await trip: &trip,
.is_ok() max_people: 10,
); notes: None,
trip_type: None,
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_ok());
let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert_eq!(trip.max_people, 10); assert_eq!(trip.max_people, 10);
@ -457,11 +488,16 @@ mod test {
let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert!( let update = trip::TripUpdate {
Trip::update_own(&pool, &cox, &trip, 10, None, Some(1), false, false) cox: &cox,
.await trip: &trip,
.is_ok() max_people: 10,
); notes: None,
trip_type: Some(1),
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_ok());
let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert_eq!(trip.max_people, 10); assert_eq!(trip.max_people, 10);
@ -481,11 +517,16 @@ mod test {
let trip = Trip::find_by_id(&pool, 1).await.unwrap(); let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert!( let update = trip::TripUpdate {
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false) cox: &cox,
.await trip: &trip,
.is_err() max_people: 10,
); notes: None,
trip_type: None,
always_show: false,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_err());
assert_eq!(trip.max_people, 1); assert_eq!(trip.max_people, 1);
} }

View File

@ -80,7 +80,7 @@ pub enum LoginError {
NotACox, NotACox,
NotATech, NotATech,
GuestNotAllowed, GuestNotAllowed,
NoPasswordSet(User), NoPasswordSet(Box<User>),
DeserializationError, DeserializationError,
} }
@ -435,7 +435,7 @@ WHERE id like ?
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE name like ? WHERE name=?
", ",
name name
) )
@ -706,7 +706,7 @@ ORDER BY last_access DESC
Err(LoginError::InvalidAuthenticationCombo) Err(LoginError::InvalidAuthenticationCombo)
} else { } else {
info!("User {name} has no PW set"); info!("User {name} has no PW set");
Err(LoginError::NoPasswordSet(user)) Err(LoginError::NoPasswordSet(Box::new(user)))
} }
} }

View File

@ -17,6 +17,17 @@ pub struct Waterlevel {
pub tumittel: i64, pub tumittel: i64,
} }
pub struct Create {
pub day: NaiveDate,
pub time: String,
pub max: i64,
pub min: i64,
pub mittel: i64,
pub tumax: i64,
pub tumin: i64,
pub tumittel: i64,
}
impl Waterlevel { impl Waterlevel {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id) sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
@ -31,20 +42,10 @@ impl Waterlevel {
.ok() .ok()
} }
pub async fn create( pub async fn create(db: &mut Transaction<'_, Sqlite>, create: &Create) -> Result<(), String> {
db: &mut Transaction<'_, Sqlite>,
day: NaiveDate,
time: String,
max: i64,
min: i64,
mittel: i64,
tumax: i64,
tumin: i64,
tumittel: i64,
) -> Result<(), String> {
sqlx::query!( sqlx::query!(
"INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)", "INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)",
day, time, max, min, mittel, tumax, tumin, tumittel create.day, create.time, create.max, create.min, create.mittel, create.tumax, create.tumin, create.tumittel
) )
.execute(db.deref_mut()) .execute(db.deref_mut())
.await .await

View File

@ -2,7 +2,7 @@ use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::waterlevel::Waterlevel; use crate::model::waterlevel::{self, Waterlevel};
pub async fn update(db: &SqlitePool) -> Result<(), String> { pub async fn update(db: &SqlitePool) -> Result<(), String> {
let mut tx = db.begin().await.unwrap(); let mut tx = db.begin().await.unwrap();
@ -29,10 +29,18 @@ pub async fn update(db: &SqlitePool) -> Result<(), String> {
let time: NaiveTime = datetime.naive_utc().time(); let time: NaiveTime = datetime.naive_utc().time();
let time_str = time.format("%H:%M").to_string(); let time_str = time.format("%H:%M").to_string();
Waterlevel::create( let create = waterlevel::Create {
&mut tx, date, time_str, max, min, mittel, tumax, tumin, tumittel, day: date,
) time: time_str,
.await? max,
min,
mittel,
tumax,
tumin,
tumittel,
};
Waterlevel::create(&mut tx, &create).await?
} }
// 3. Save in DB // 3. Save in DB
@ -77,25 +85,23 @@ fn fetch() -> Result<Station, String> {
if let Ok(data) = forecast { if let Ok(data) = forecast {
if data.len() == 1 { if data.len() == 1 {
return Ok(data[0].clone()); Ok(data[0].clone())
} else { } else {
return Err(format!( Err(format!(
"Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?", "Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?",
data.len() data.len()
)); ))
} }
} else { } else {
return Err(format!( Err(format!(
"Failed to parse the json received by {url}: {}", "Failed to parse the json received by {url}: {}",
forecast.err().unwrap() forecast.err().unwrap()
)); ))
} }
} }
Err(_) => { Err(_) => Err(format!(
return Err(format!( "Could not fetch {url}, do you have internet? Maybe their server is down?"
"Could not fetch {url}, do you have internet? Maybe their server is down?" )),
));
}
} }
} }

View File

@ -103,18 +103,16 @@ fn fetch(api_key: &str) -> Result<Data, String> {
let data: Result<Data, _> = response.into_json(); let data: Result<Data, _> = response.into_json();
if let Ok(data) = data { if let Ok(data) = data {
return Ok(data); Ok(data)
} else { } else {
return Err(format!( Err(format!(
"Failed to parse the json received by {url}: {}", "Failed to parse the json received by {url}: {}",
data.err().unwrap() data.err().unwrap()
)); ))
} }
} }
Err(_) => { Err(_) => Err(format!(
return Err(format!( "Could not fetch {url}, do you have internet? Maybe their server is down?"
"Could not fetch {url}, do you have internet? Maybe their server is down?" )),
));
}
} }
} }

View File

@ -8,7 +8,7 @@ use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
planned_event::PlannedEvent, planned_event::{self, PlannedEvent},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::PlannedEventUser, user::PlannedEventUser,
}; };
@ -57,19 +57,17 @@ async fn update(
data: Form<UpdatePlannedEventForm<'_>>, data: Form<UpdatePlannedEventForm<'_>>,
_admin: PlannedEventUser, _admin: PlannedEventUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let update = planned_event::EventUpdate {
name: data.name,
planned_amount_cox: data.planned_amount_cox,
max_people: data.max_people,
notes: data.notes,
always_show: data.always_show,
is_locked: data.is_locked,
};
match PlannedEvent::find_by_id(db, data.id).await { match PlannedEvent::find_by_id(db, data.id).await {
Some(planned_event) => { Some(planned_event) => {
planned_event planned_event.update(db, &update).await;
.update(
db,
data.name,
data.planned_amount_cox,
data.max_people,
data.notes,
data.always_show,
data.is_locked,
)
.await;
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet") Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
} }
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"), None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),

View File

@ -39,7 +39,6 @@ struct LoginForm<'r> {
password: &'r str, password: &'r str,
} }
#[derive(Debug)]
pub struct UserAgent(String); pub struct UserAgent(String);
#[rocket::async_trait] #[rocket::async_trait]
@ -83,8 +82,8 @@ async fn login(
Log::create( Log::create(
db, db,
format!( format!(
"Succ login of {} with this useragent: {:?}", "Succ login of {} with this useragent: {}",
login.name, agent login.name, agent.0
), ),
) )
.await; .await;

View File

@ -9,7 +9,7 @@ use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
log::Log, log::Log,
planned_event::PlannedEvent, planned_event::PlannedEvent,
trip::{CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError}, trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::CoxUser, user::CoxUser,
}; };
@ -54,18 +54,16 @@ async fn update(
cox: CoxUser, cox: CoxUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
if let Some(trip) = Trip::find_by_id(db, trip_id).await { if let Some(trip) = Trip::find_by_id(db, trip_id).await {
match Trip::update_own( let update = trip::TripUpdate {
db, cox: &cox,
&cox, trip: &trip,
&trip, max_people: data.max_people,
data.max_people, notes: data.notes,
data.notes, trip_type: data.trip_type,
data.trip_type, always_show: data.always_show,
data.always_show, is_locked: data.is_locked,
data.is_locked, };
) match Trip::update_own(db, &update).await {
.await
{
Ok(_) => Flash::success( Ok(_) => Flash::success(
Redirect::to("/planned"), Redirect::to("/planned"),
"Ausfahrt erfolgreich aktualisiert.", "Ausfahrt erfolgreich aktualisiert.",
@ -97,6 +95,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
.await; .await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!") Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
} }
Err(CoxHelpError::CanceledEvent) => {
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
}
Err(CoxHelpError::AlreadyRegisteredAsCox) => { Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!") Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
} }

View File

@ -27,7 +27,7 @@ use crate::model::{
user::{AdminUser, DonauLinzUser, User, UserWithDetails}, user::{AdminUser, DonauLinzUser, User, UserWithDetails},
}; };
pub struct KioskCookie(String); pub struct KioskCookie(());
#[rocket::async_trait] #[rocket::async_trait]
impl<'r> FromRequest<'r> for KioskCookie { impl<'r> FromRequest<'r> for KioskCookie {
@ -35,7 +35,7 @@ impl<'r> FromRequest<'r> for KioskCookie {
async fn from_request(request: &'r Request<'_>) -> request::Outcome<KioskCookie, Self::Error> { async fn from_request(request: &'r Request<'_>) -> request::Outcome<KioskCookie, Self::Error> {
match request.cookies().get_private("kiosk") { match request.cookies().get_private("kiosk") {
Some(cookie) => request::Outcome::Success(KioskCookie(cookie.value().to_string())), Some(_) => request::Outcome::Success(KioskCookie(())),
None => request::Outcome::Forward(rocket::http::Status::SeeOther), None => request::Outcome::Forward(rocket::http::Status::SeeOther),
} }
} }

View File

@ -8,7 +8,9 @@
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert"> role="alert">
<h2 class="h2">Gruppe</h2> <h2 class="h2">Gruppe</h2>
<form action="/admin/notification/group" method="post" class="grid gap-3 p-3"> <form action="/admin/notification/group"
method="post"
class="grid gap-3 p-3">
{{ macros::select(label="Gruppe", data=roles, name="role_id") }} {{ macros::select(label="Gruppe", data=roles, name="role_id") }}
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }} {{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }} {{ macros::input(label="Nachricht", name="message", type="text", required=true) }}
@ -18,7 +20,9 @@
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert"> role="alert">
<h2 class="h2">Person</h2> <h2 class="h2">Person</h2>
<form action="/admin/notification/user" method="post" class="grid gap-3 p-3"> <form action="/admin/notification/user"
method="post"
class="grid gap-3 p-3">
{{ macros::select(label="Person", data=users, name="user_id") }} {{ macros::select(label="Person", data=users, name="user_id") }}
{{ macros::input(label="Überschrift", name="category", type="text", required=true) }} {{ macros::input(label="Überschrift", name="category", type="text", required=true) }}
{{ macros::input(label="Nachricht", name="message", type="text", required=true) }} {{ macros::input(label="Nachricht", name="message", type="text", required=true) }}

View File

@ -46,20 +46,42 @@
<div class="p-3"> <div class="p-3">
Folgende personenbezogenen haben wir von dir gespeichert: Folgende personenbezogenen haben wir von dir gespeichert:
<ul> <ul>
<li><strong>Name:</strong> {{ loggedin_user.name }}</li> <li>
<li><strong>Passwort:</strong> (verschlüsselt als argon Hash)</li> <strong>Name:</strong> {{ loggedin_user.name }}
<li><strong>Letzter Zugriff:</strong> {{ loggedin_user.last_access }}</li> </li>
<li><strong>Mitglied seit:</strong> {{ loggedin_user.member_since_date }}</li> <li>
<li><strong>Geburtsdatum:</strong> {{ loggedin_user.birthdate }}</li> <strong>Passwort:</strong> (verschlüsselt als argon Hash)
<li><strong>Mail:</strong> {{ loggedin_user.mail }}</li> </li>
{% if loggedin_user.nickname %}<li><strong>Spitzname:</strong> {{ loggedin_user.nickname }}</li>{% endif %} <li>
<li><strong>Telefonnummer:</strong> {{ loggedin_user.phone }}</li> <strong>Letzter Zugriff:</strong> {{ loggedin_user.last_access }}
<li><strong>Adresse:</strong> {{ loggedin_user.address }}</li> </li>
<li>
<strong>Mitglied seit:</strong> {{ loggedin_user.member_since_date }}
</li>
<li>
<strong>Geburtsdatum:</strong> {{ loggedin_user.birthdate }}
</li>
<li>
<strong>Mail:</strong> {{ loggedin_user.mail }}
</li>
{% if loggedin_user.nickname %}
<li>
<strong>Spitzname:</strong> {{ loggedin_user.nickname }}
</li>
{% endif %}
<li>
<strong>Telefonnummer:</strong> {{ loggedin_user.phone }}
</li>
<li>
<strong>Adresse:</strong> {{ loggedin_user.address }}
</li>
<li>(Beitrittserklärung)</li> <li>(Beitrittserklärung)</li>
{% if loggedin_user.family_id %} {% if loggedin_user.family_id %}
<li>Verbindung zu Familienmitglied (gespeichert um Familientarif anstatt Vollmitglied zu haben)</li> <li>Verbindung zu Familienmitglied (gespeichert um Familientarif anstatt Vollmitglied zu haben)</li>
{% endif %} {% endif %}
<li><strong>Rollen:</strong> {{ loggedin_user.roles }} (werden für verschiedene Funktionen im Ruderassistenten verwendet)</li> <li>
<strong>Rollen:</strong> {{ loggedin_user.roles }} (werden für verschiedene Funktionen im Ruderassistenten verwendet)
</li>
<li>Anmeldungen zu Ausfahrten</li> <li>Anmeldungen zu Ausfahrten</li>
<li>Anmeldungen zu Events (zB Fetzenfahrt, Anrudern, USI-Rudern, ...)</li> <li>Anmeldungen zu Events (zB Fetzenfahrt, Anrudern, USI-Rudern, ...)</li>
<li>Logbucheinträge</li> <li>Logbucheinträge</li>

View File

@ -1,38 +1,39 @@
<footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3"> <footer class="bg-primary-950 dark:bg-primary-900 text-white w-full flex justify-center p-3">
<div class="max-w-screen-xl"> <div class="max-w-screen-xl">
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div> <div>
<span class="text-[#ff0000]">&hearts;</span> <span class="text-[#ff0000]">&hearts;</span>
Erstellt vom ASKÖ Ruderverein Donau Linz <a class="underline" onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');">... und dir?</a> Erstellt vom ASKÖ Ruderverein Donau Linz <a class="underline"
</div> onclick="alert('Wir suchen kreative und motivierte Köpfe, die diesen Ruderassistenten mitgestalten möchten. Das Backend ist in Rust (Rocket), das Frontend in TypeScript und Teraform, wobei wir mit dem Gedanken spielen, zu Svelte(Kit) zu wechseln.\n\nWenn du Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird, dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.\n\nWirf den Anker bei uns ausi und melde dich bei Marie oder Philipp oder it@rudernlinz.at für eine Zukunft ohne optische Kenterung in Form von hässlichen Alerts ;)');">... und dir?</a>
<div> </div>
<button id="theme-toggle-js" <div>
type="button" <button id="theme-toggle-js"
data-theme="light" type="button"
class="btn btn-primary"> data-theme="light"
<span class="hidden dark:inline"> class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" <span class="hidden dark:inline">
width="16" <svg xmlns="http://www.w3.org/2000/svg"
height="16" width="16"
fill="currentColor" height="16"
viewBox="0 0 16 16"> fill="currentColor"
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" /> viewBox="0 0 16 16">
</svg> <path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
</span> </svg>
<span class="inline dark:hidden"> </span>
<svg xmlns="http://www.w3.org/2000/svg" <span class="inline dark:hidden">
width="16" <svg xmlns="http://www.w3.org/2000/svg"
height="16" width="16"
fill="currentColor" height="16"
viewBox="0 0 16 16"> fill="currentColor"
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" /> viewBox="0 0 16 16">
</svg> <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z" />
</span> </svg>
</button> </span>
</div> </button>
</div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<a class="underline" href="/impressum">Impressum</a> <a class="underline" href="/impressum">Impressum</a>
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -99,15 +99,27 @@
style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}"> style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="mr-1"> <div class="mr-1">
<strong class="text-primary-900 dark:text-white"> {% if planned_event.max_people == 0 %}
{{ planned_event.planned_starting_time }} <strong class="text-[#f43f5e]">&#9888; Absage
Uhr {{ planned_event.planned_starting_time }}
</strong> Uhr
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }} </strong>
{%- if planned_event.trip_type %} <small class="text-[#f43f5e]">({{ planned_event.name }}
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }} {%- if planned_event.trip_type %}
{%- endif -%} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
)</small> {%- endif -%}
)</small>
{% else %}
<strong class="text-primary-900 dark:text-white">
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}
{%- if planned_event.trip_type %}
- {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}
{%- endif -%}
)</small>
{% endif %}
<br /> <br />
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}) <a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }})
{% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %} {% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}
@ -165,16 +177,22 @@
<div id="event{{ planned_event.trip_details_id }}"> <div id="event{{ planned_event.trip_details_id }}">
{# --- START List Coxes --- #} {# --- START List Coxes --- #}
{% if planned_event.planned_amount_cox > 0 %} {% if planned_event.planned_amount_cox > 0 %}
{% if amount_cox_missing > 0 %} {% if planned_event.max_people == 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }} {{ macros::box(participants=planned_event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
{% else %} {% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }} {% if amount_cox_missing > 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{# --- END List Coxes --- #} {# --- END List Coxes --- #}
{# --- START List Rowers --- #} {# --- START List Rowers --- #}
{% if planned_event.max_people > 0 %} {% set amount_cur_rower = planned_event.rower | length %}
{% set amount_cur_rower = planned_event.rower | length %} {% if planned_event.max_people == 0 %}
{{ macros::box(header='Absage', bg='[#f43f5e]', participants=planned_event.rower, trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% else %}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }} {{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% endif %} {% endif %}
{# --- END List Rowers --- #} {# --- END List Rowers --- #}
@ -208,13 +226,35 @@
</div> </div>
{# --- END Edit Form --- #} {# --- END Edit Form --- #}
{# --- START Delete Btn --- #} {# --- START Delete Btn --- #}
<div class="text-right"> {% if planned_event.rower | length == 0 and amount_cur_cox == 0 %}
<a href="/admin/planned-event/{{ planned_event.id }}/delete" <div class="text-right mt-6">
class="inline-block btn btn-alert"> <a href="/admin/planned-event/{{ planned_event.id }}/delete"
{% include "includes/delete-icon" %} class="inline-block btn btn-alert">
Termin löschen {% include "includes/delete-icon" %}
</a> Termin löschen
</div> </a>
</div>
{% else %}
{% if planned_event.max_people == 0 %}
Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
{% else %}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Event absagen</h3>
<form action="/admin/planned-event" method="post" class="grid">
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="id" value="{{ planned_event.id }}" />
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
{{ macros::input(label='', name='name', type='hidden', value=planned_event.name) }}
{{ macros::input(label='', name='max_people', type='hidden', value=planned_event.max_people) }}
{{ macros::input(label='', name='planned_amount_cox', type='hidden', value=planned_event.planned_amount_cox) }}
{{ macros::input(label='', name='notes', type='hidden', value=planned_event.notes) }}
{{ macros::input(label='', name='always_show', type='hidden', value=planned_event.always_show) }}
{{ macros::input(label='', name='is_locked', type='hidden', value=planned_event.is_locked) }}
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
</form>
</div>
{% endif %}
{% endif %}
{% endif %} {% endif %}
{# --- END Delete Btn --- #} {# --- END Delete Btn --- #}
</div> </div>