Merge pull request 'updates' (#714) from updates into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m4s
CI/CD Pipeline / deploy-staging (push) Successful in 8m13s
CI/CD Pipeline / deploy-main (push) Has been skipped

Reviewed-on: #714
This commit is contained in:
philipp 2024-09-02 13:26:43 +02:00
commit 1202b0afec
17 changed files with 217 additions and 78 deletions

View File

@ -18,7 +18,7 @@ test("cox can create and delete trip", async ({ page }) => {
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details"); await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
await page.goto("/planned"); await page.goto("/planned");
await page.getByRole("link", { name: "Details" }).click(); await page.getByRole('link', { name: 'Details' }).nth(1).click();
await page.getByRole("link", { name: "Termin löschen" }).click(); await page.getByRole("link", { name: "Termin löschen" }).click();
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!"); await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
}); });
@ -52,11 +52,11 @@ test.describe("cox can edit trips", () => {
test("edit remarks", async () => { test("edit remarks", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.locator("#sidebar #notes").click(); await sharedPage.locator("#sidebar #notes").click();
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung"); await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
await sharedPage.getByRole("button", { name: "Speichern" }).click(); await sharedPage.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Meine Anmerkung", "Meine Anmerkung",
); );
@ -68,14 +68,14 @@ test.describe("cox can edit trips", () => {
test("add and remove guest", async () => { test("add and remove guest", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.locator("#sidebar #user_note").click(); await sharedPage.locator("#sidebar #user_note").click();
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast"); await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click(); await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(sharedPage.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich angemeldet!", "Erfolgreich angemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 4", "Freie Plätze: 4",
); );
@ -90,7 +90,7 @@ test.describe("cox can edit trips", () => {
await expect(sharedPage.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich abgemeldet!", "Erfolgreich abgemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
@ -108,7 +108,7 @@ test.describe("cox can edit trips", () => {
test("change amount rower", async () => { test("change amount rower", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
@ -122,7 +122,7 @@ test.describe("cox can edit trips", () => {
test("call off trip", async () => { test("call off trip", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 3", "Freie Plätze: 3",
); );
@ -137,7 +137,7 @@ test.describe("cox can edit trips", () => {
test.afterAll(async () => { test.afterAll(async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.getByRole("link", { name: "Termin löschen" }).click(); await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
await sharedPage.close(); await sharedPage.close();
}); });

View File

@ -45,10 +45,10 @@ INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6); INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9); INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'trip_details for a planned event');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2); INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅');

View File

@ -233,6 +233,7 @@ WHERE trip_details.id=?
db: &SqlitePool, db: &SqlitePool,
name: &str, name: &str,
planned_amount_cox: i32, planned_amount_cox: i32,
always_show: bool,
trip_details: &TripDetails, trip_details: &TripDetails,
) { ) {
if trip_details.always_show { if trip_details.always_show {
@ -245,6 +246,10 @@ WHERE trip_details.id=?
.await; .await;
} }
if always_show && !trip_details.always_show {
trip_details.set_always_show(db, true).await;
}
sqlx::query!( sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)", "INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
name, name,
@ -433,14 +438,14 @@ mod test {
use crate::{model::tripdetails::TripDetails, testdb}; use crate::{model::tripdetails::TripDetails, testdb};
use super::Event; use super::Event;
use chrono::NaiveDate; use chrono::Local;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[sqlx::test] #[sqlx::test]
fn test_get_day() { fn test_get_day() {
let pool = testdb!(); let pool = testdb!();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
} }
@ -450,9 +455,9 @@ mod test {
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await; Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
} }
@ -463,7 +468,7 @@ mod test {
planned_event.delete(&pool).await.unwrap(); planned_event.delete(&pool).await.unwrap();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 0); assert_eq!(res.len(), 0);
} }
@ -471,7 +476,8 @@ mod test {
fn test_ics() { fn test_ics() {
let pool = testdb!(); let pool = testdb!();
let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await; let actual = Event::get_ics_feed(&pool).await;
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual); assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
} }
} }

View File

@ -575,10 +575,8 @@ ORDER BY departure DESC
return Err(LogbookUpdateError::ArrivalNotAfterDeparture); return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
} }
if !boat.external { if !boat.external && boat.on_water_between(db, dep, arr).await {
if boat.on_water_between(db, dep, arr).await { return Err(LogbookUpdateError::BoatAlreadyOnWater);
return Err(LogbookUpdateError::BoatAlreadyOnWater);
};
} }
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60; let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
@ -594,14 +592,13 @@ ORDER BY departure DESC
let today = Local::now().date_naive(); let today = Local::now().date_naive();
let day_diff = today - arr.date(); let day_diff = today - arr.date();
let day_diff = day_diff.num_days(); let day_diff = day_diff.num_days();
if day_diff >= 7 { if day_diff >= 7
if !user.has_role_tx(db, "admin").await && !user.has_role_tx(db, "admin").await
&& !user && !user
.has_role_tx(db, "allow-retroactive-logbookentries") .has_role_tx(db, "allow-retroactive-logbookentries")
.await .await
{ {
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday); return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
}
} }
if day_diff < 0 && !user.has_role_tx(db, "admin").await { if day_diff < 0 && !user.has_role_tx(db, "admin").await {
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday); return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);

View File

@ -203,6 +203,7 @@ mod test {
testdb, testdb,
}; };
use chrono::Local;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[sqlx::test] #[sqlx::test]
@ -213,17 +214,16 @@ mod test {
let add_tripdetails = TripDetailsToAdd { let add_tripdetails = TripDetailsToAdd {
planned_starting_time: "10:00", planned_starting_time: "10:00",
max_people: 4, max_people: 4,
day: "1970-02-01".into(), day: Local::now().date_naive().format("%Y-%m-%d").to_string(),
notes: None, notes: None,
trip_type: None, trip_type: None,
allow_guests: false, allow_guests: false,
always_show: false,
}; };
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await; let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id) let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
.await .await
.unwrap(); .unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await; Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
let event = Event::find_by_trip_details(&pool, trip_details.id) let event = Event::find_by_trip_details(&pool, trip_details.id)
.await .await
.unwrap(); .unwrap();

View File

@ -40,7 +40,6 @@ pub struct TripUpdate<'a> {
pub max_people: i32, pub max_people: i32,
pub notes: Option<&'a str>, pub notes: Option<&'a str>,
pub trip_type: Option<i64>, //TODO: Move to `TripType` pub trip_type: Option<i64>, //TODO: Move to `TripType`
pub always_show: bool,
pub is_locked: bool, pub is_locked: bool,
} }
@ -83,7 +82,7 @@ impl Trip {
} }
// don't notify people who have cancelled their trip // don't notify people who have cancelled their trip
if notify.cancelled(db) { if notify.cancelled() {
continue; continue;
} }
@ -217,11 +216,10 @@ WHERE day=?
let was_already_cancelled = tripdetails.max_people == 0; 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 = ?, is_locked = ? WHERE id = ?",
update.max_people, update.max_people,
update.notes, update.notes,
update.trip_type, update.trip_type,
update.always_show,
update.is_locked, update.is_locked,
trip_details_id trip_details_id
) )
@ -345,6 +343,20 @@ WHERE day=?
self.cox_id == user_id self.cox_id == user_id
} }
pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) {
if let Some(trip_details) = self.trip_details_id {
let new_state = !self.always_show;
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
new_state,
trip_details
)
.execute(db)
.await
.unwrap();
}
}
pub(crate) async fn get_pinned_for_day( pub(crate) async fn get_pinned_for_day(
db: &sqlx::Pool<sqlx::Sqlite>, db: &sqlx::Pool<sqlx::Sqlite>,
day: NaiveDate, day: NaiveDate,
@ -394,7 +406,7 @@ mod test {
testdb, testdb,
}; };
use chrono::NaiveDate; use chrono::Local;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use super::Trip; use super::Trip;
@ -421,7 +433,8 @@ mod test {
fn test_get_day_cox_trip() { fn test_get_day_cox_trip() {
let pool = testdb!(); let pool = testdb!();
let res = Trip::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()).await; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let res = Trip::get_for_day(&pool, tomorrow).await;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
} }
@ -477,7 +490,6 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: None, trip_type: None,
always_show: false,
is_locked: false, is_locked: false,
}; };
@ -506,7 +518,6 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: Some(1), trip_type: Some(1),
always_show: false,
is_locked: false, is_locked: false,
}; };
assert!(Trip::update_own(&pool, &update).await.is_ok()); assert!(Trip::update_own(&pool, &update).await.is_ok());
@ -535,7 +546,6 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: None, trip_type: None,
always_show: false,
is_locked: false, is_locked: false,
}; };
assert!(Trip::update_own(&pool, &update).await.is_err()); assert!(Trip::update_own(&pool, &update).await.is_err());

View File

@ -1,5 +1,5 @@
use crate::model::user::User; use crate::model::user::User;
use chrono::NaiveDate; use chrono::{Local, NaiveDate};
use rocket::FromForm; use rocket::FromForm;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
@ -33,7 +33,6 @@ pub struct TripDetailsToAdd<'r> {
pub notes: Option<&'r str>, pub notes: Option<&'r str>,
pub trip_type: Option<i64>, pub trip_type: Option<i64>,
pub allow_guests: bool, pub allow_guests: bool,
pub always_show: bool,
} }
impl TripDetails { impl TripDetails {
@ -59,6 +58,24 @@ WHERE id like ?
} }
} }
pub fn date(&self) -> NaiveDate {
NaiveDate::parse_from_str(&self.day, "%Y-%m-%d").unwrap()
}
pub(crate) async fn user_sees_trip(&self, db: &SqlitePool, user: &User) -> bool {
let today = Local::now().date_naive();
let day_diff = self.date() - today;
let day_diff = day_diff.num_days();
if day_diff < 0 {
// tripdetails is in past
return false;
}
if day_diff <= user.amount_days_to_show(db).await {
return true;
}
self.always_show
}
pub async fn find_by_startingdatetime( pub async fn find_by_startingdatetime(
db: &SqlitePool, db: &SqlitePool,
day: String, day: String,
@ -77,7 +94,7 @@ WHERE day = ? AND planned_starting_time = ?
.await.unwrap() .await.unwrap()
} }
pub fn cancelled(&self, db: &SqlitePool) -> bool { pub fn cancelled(&self) -> bool {
self.max_people == 0 self.max_people == 0
} }
@ -89,7 +106,7 @@ WHERE day = ? AND planned_starting_time = ?
return; return;
} }
if self.cancelled(db) { if self.cancelled() {
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending // Cox cancelled event, thus it's probably bad weather. Don't bother with sending
// notifications // notifications
return; return;
@ -146,14 +163,13 @@ WHERE day = ? AND planned_starting_time = ?
/// Creates a new entry in `trip_details` and returns its id. /// Creates a new entry in `trip_details` and returns its id.
pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 { pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
let query = sqlx::query!( let query = sqlx::query!(
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show) VALUES(?, ?, ?, ?, ?, ?, ?)" , "INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id) VALUES(?, ?, ?, ?, ?, ?)" ,
tripdetails.planned_starting_time, tripdetails.planned_starting_time,
tripdetails.max_people, tripdetails.max_people,
tripdetails.day, tripdetails.day,
tripdetails.notes, tripdetails.notes,
tripdetails.allow_guests, tripdetails.allow_guests,
tripdetails.trip_type, tripdetails.trip_type,
tripdetails.always_show
) )
.execute(db) .execute(db)
.await .await
@ -161,6 +177,17 @@ WHERE day = ? AND planned_starting_time = ?
query.last_insert_rowid() query.last_insert_rowid()
} }
pub async fn set_always_show(&self, db: &SqlitePool, value: bool) {
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
value,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
}
pub async fn is_full(&self, db: &SqlitePool) -> bool { pub async fn is_full(&self, db: &SqlitePool) -> bool {
let amount_currently_registered = sqlx::query!( let amount_currently_registered = sqlx::query!(
"SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?", "SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?",
@ -309,7 +336,6 @@ mod test {
notes: None, notes: None,
allow_guests: false, allow_guests: false,
trip_type: None, trip_type: None,
always_show: false
} }
) )
.await, .await,
@ -325,7 +351,6 @@ mod test {
notes: None, notes: None,
allow_guests: false, allow_guests: false,
trip_type: None, trip_type: None,
always_show: false
} }
) )
.await, .await,

View File

@ -450,6 +450,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
false false
} }
pub async fn allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool {
AllowedToUpdateTripToAlwaysBeShownUser::new(db, self.clone())
.await
.is_some()
}
pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool { pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool {
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id) match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
.fetch_one(db) .fetch_one(db)
@ -879,18 +885,32 @@ ORDER BY last_access DESC
days days
} }
async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 { pub(crate) async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
if self.has_role(db, "cox").await { if self.has_role(db, "cox").await {
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok, let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
//december //december
//has 31 //has 31
//days //days
end_of_year let days_left_in_year = end_of_year
.signed_duration_since(Local::now().date_naive()) .signed_duration_since(Local::now().date_naive())
.num_days() .num_days()
+ 1 + 1;
if days_left_in_year <= 31 {
let end_of_next_year =
NaiveDate::from_ymd_opt(Local::now().year() + 1, 12, 31).unwrap(); //Ok,
//december
//has 31
//days
end_of_next_year
.signed_duration_since(Local::now().date_naive())
.num_days()
+ 1
} else {
days_left_in_year
}
} else { } else {
6 10
} }
} }
@ -1014,6 +1034,7 @@ special_user!(VorstandUser, +"Vorstand");
special_user!(EventUser, +"manage_events"); special_user!(EventUser, +"manage_events");
special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin"); special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
special_user!(ManageUserUser, +"admin", +"schriftfuehrer"); special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)] #[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
pub struct UserWithRolesAndMembershipPdf { pub struct UserWithRolesAndMembershipPdf {

View File

@ -24,6 +24,10 @@ impl UserTrip {
return Err(UserTripError::GuestNotAllowedForThisEvent); return Err(UserTripError::GuestNotAllowedForThisEvent);
} }
if !trip_details.user_sees_trip(db, user).await {
return Err(UserTripError::NotVisibleToUser);
}
//TODO: Check if user sees the event (otherwise she could forge trip_details_id) //TODO: Check if user sees the event (otherwise she could forge trip_details_id)
let is_cox = trip_details.user_is_cox(db, user).await; let is_cox = trip_details.user_is_cox(db, user).await;
@ -96,6 +100,10 @@ impl UserTrip {
return Err(UserTripDeleteError::DetailsLocked); return Err(UserTripDeleteError::DetailsLocked);
} }
if !trip_details.user_sees_trip(db, user).await {
return Err(UserTripDeleteError::NotVisibleToUser);
}
if let Some(name) = name { if let Some(name) = name {
if !trip_details.user_allowed_to_change(db, user).await { if !trip_details.user_allowed_to_change(db, user).await {
return Err(UserTripDeleteError::NotAllowedToDeleteGuest); return Err(UserTripDeleteError::NotAllowedToDeleteGuest);
@ -137,6 +145,7 @@ pub enum UserTripError {
CantRegisterAtOwnEvent, CantRegisterAtOwnEvent,
GuestNotAllowedForThisEvent, GuestNotAllowedForThisEvent,
NotAllowedToAddGuest, NotAllowedToAddGuest,
NotVisibleToUser,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -144,6 +153,7 @@ pub enum UserTripDeleteError {
DetailsLocked, DetailsLocked,
GuestNotParticipating, GuestNotParticipating,
NotAllowedToDeleteGuest, NotAllowedToDeleteGuest,
NotVisibleToUser,
} }
#[cfg(test)] #[cfg(test)]

View File

@ -18,6 +18,7 @@ use crate::model::{
struct AddEventForm<'r> { struct AddEventForm<'r> {
name: &'r str, name: &'r str,
planned_amount_cox: i32, planned_amount_cox: i32,
always_show: bool,
tripdetails: TripDetailsToAdd<'r>, tripdetails: TripDetailsToAdd<'r>,
} }
@ -34,7 +35,14 @@ async fn create(
//just created //just created
//the object //the object
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await; Event::create(
db,
data.name,
data.planned_amount_cox,
data.always_show,
&trip_details,
)
.await;
Flash::success(Redirect::to("/planned"), "Event hinzugefügt") Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
} }

View File

@ -374,7 +374,7 @@ async fn create_scheckbuch(
if mail.parse::<Address>().is_err() { if mail.parse::<Address>().is_err() {
return Flash::error( return Flash::error(
Redirect::to("/admin/user/scheckbuch"), Redirect::to("/admin/user/scheckbuch"),
format!("Keine gültige Mailadresse"), "Keine gültige Mailadresse".to_string(),
); );
} }
@ -383,9 +383,8 @@ async fn create_scheckbuch(
if User::find_by_name(db, name).await.is_some() { if User::find_by_name(db, name).await.is_some() {
return Flash::error( return Flash::error(
Redirect::to("/admin/user/scheckbuch"), Redirect::to("/admin/user/scheckbuch"),
format!( "Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet"
"Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet" .to_string(),
),
); );
} }
@ -418,14 +417,14 @@ async fn schnupper_to_scheckbuch(
let Some(user) = User::find_by_id(db, id).await else { let Some(user) = User::find_by_id(db, id).await else {
return Flash::error( return Flash::error(
Redirect::to("/admin/schnupper"), Redirect::to("/admin/schnupper"),
format!("user id not found"), "user id not found".to_string(),
); );
}; };
if !user.has_role(db, "schnupperant").await { if !user.has_role(db, "schnupperant").await {
return Flash::error( return Flash::error(
Redirect::to("/admin/schnupper"), Redirect::to("/admin/schnupper"),
format!("kein schnupperant..."), "kein schnupperant...".to_string(),
); );
} }

View File

@ -11,7 +11,7 @@ use crate::model::{
log::Log, log::Log,
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError}, trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::CoxUser, user::{AllowedToUpdateTripToAlwaysBeShownUser, CoxUser},
}; };
#[post("/trip", data = "<data>")] #[post("/trip", data = "<data>")]
@ -42,7 +42,6 @@ struct EditTripForm<'r> {
max_people: i32, max_people: i32,
notes: Option<&'r str>, notes: Option<&'r str>,
trip_type: Option<i64>, trip_type: Option<i64>,
always_show: bool,
is_locked: bool, is_locked: bool,
} }
@ -60,7 +59,6 @@ async fn update(
max_people: data.max_people, max_people: data.max_people,
notes: data.notes, notes: data.notes,
trip_type: data.trip_type, trip_type: data.trip_type,
always_show: data.always_show,
is_locked: data.is_locked, is_locked: data.is_locked,
}; };
match Trip::update_own(db, &update).await { match Trip::update_own(db, &update).await {
@ -80,6 +78,23 @@ async fn update(
} }
} }
#[get("/trip/<trip_id>/toggle-always-show")]
async fn toggle_always_show(
db: &State<SqlitePool>,
trip_id: i64,
_user: AllowedToUpdateTripToAlwaysBeShownUser,
) -> Flash<Redirect> {
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
trip.toggle_always_show(db).await;
Flash::success(
Redirect::to("/planned"),
"'Immer anzeigen' erfolgreich gesetzt!",
)
} else {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
}
}
#[get("/join/<planned_event_id>")] #[get("/join/<planned_event_id>")]
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> { async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await { if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
@ -164,12 +179,19 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![create, join, remove, remove_trip, update] routes![
create,
join,
remove,
remove_trip,
update,
toggle_always_show
]
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use chrono::NaiveDate; use chrono::{Local, NaiveDate};
use rocket::{ use rocket::{
http::{ContentType, Status}, http::{ContentType, Status},
local::asynchronous::Client, local::asynchronous::Client,
@ -230,7 +252,9 @@ mod test {
fn test_trip_update_succ() { fn test_trip_update_succ() {
let db = testdb!(); let db = testdb!();
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0]; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
println!("{tomorrow}");
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people); assert_eq!(1, trip.trip.max_people);
assert_eq!( assert_eq!(
"trip_details for trip from cox", "trip_details for trip from cox",
@ -266,7 +290,8 @@ mod test {
"7:successAusfahrt erfolgreich aktualisiert." "7:successAusfahrt erfolgreich aktualisiert."
); );
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0]; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(12, trip.trip.max_people); assert_eq!(12, trip.trip.max_people);
assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap()); assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap());
} }
@ -306,7 +331,9 @@ mod test {
fn test_trip_update_wrong_cox() { fn test_trip_update_wrong_cox() {
let db = testdb!(); let db = testdb!();
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0]; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people); assert_eq!(1, trip.trip.max_people);
assert_eq!( assert_eq!(
"trip_details for trip from cox", "trip_details for trip from cox",

View File

@ -131,13 +131,13 @@ async fn new_blogpost(
blogpost: Form<NewBlogpostForm<'_>>, blogpost: Form<NewBlogpostForm<'_>>,
config: &State<Config>, config: &State<Config>,
) -> String { ) -> String {
if blogpost.pw == &config.wordpress_key { if blogpost.pw == config.wordpress_key {
let member = Role::find_by_name(&db, "Donau Linz").await.unwrap(); let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
Notification::create_for_role( Notification::create_for_role(
db, db,
&member, &member,
&urlencoding::decode(blogpost.article_title).expect("UTF-8"), &urlencoding::decode(blogpost.article_title).expect("UTF-8"),
&format!("Neuer Blogpost"), "Neuer Blogpost",
Some(blogpost.article_url), Some(blogpost.article_url),
None, None,
) )
@ -160,9 +160,9 @@ async fn blogpost_unpublished(
blogpost: Form<BlogpostUnpublishedForm<'_>>, blogpost: Form<BlogpostUnpublishedForm<'_>>,
config: &State<Config>, config: &State<Config>,
) -> String { ) -> String {
if blogpost.pw == &config.wordpress_key { if blogpost.pw == config.wordpress_key {
Notification::delete_by_link( Notification::delete_by_link(
&db, db,
&urlencoding::decode(blogpost.article_url).expect("UTF-8"), &urlencoding::decode(blogpost.article_url).expect("UTF-8"),
) )
.await; .await;

View File

@ -37,6 +37,10 @@ async fn index(
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
} }
context.insert(
"allowed_to_update_always_show_trip",
&user.allowed_to_update_always_show_trip(db).await,
);
context.insert("fee", &user.fee(db).await); context.insert("fee", &user.fee(db).await);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("days", &days); context.insert("days", &days);
@ -99,6 +103,10 @@ async fn join(
Redirect::to("/planned"), Redirect::to("/planned"),
"Du darfst keine Gäste hinzufügen.", "Du darfst keine Gäste hinzufügen.",
), ),
Err(UserTripError::NotVisibleToUser) => Flash::error(
Redirect::to("/planned"),
"Du kannst dich nicht registrieren, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripError::DetailsLocked) => Flash::error( Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/planned"), Redirect::to("/planned"),
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.", "Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
@ -147,6 +155,10 @@ async fn remove_guest(
Err(UserTripDeleteError::GuestNotParticipating) => { Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.") Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
} }
Err(UserTripDeleteError::NotVisibleToUser) => Flash::error(
Redirect::to("/planned"),
"Du kannst dich nicht abmelden, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error( Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/planned"), Redirect::to("/planned"),
"Keine Berechtigung um den Gast zu entfernen.", "Keine Berechtigung um den Gast zu entfernen.",
@ -191,6 +203,18 @@ async fn remove(
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
} }
Err(UserTripDeleteError::NotVisibleToUser) => {
Log::create(
db,
format!(
"User {} tried to unregister for not-yet-seeable trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/planned"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
}
Err(_) => { Err(_) => {
panic!("Not possible to be here"); panic!("Not possible to be here");
} }

View File

@ -10,7 +10,7 @@
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }} {{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }}
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='tripdetails.max_people', type='number', required=true, min='0') }} {{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='tripdetails.max_people', type='number', required=true, min='0') }}
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='tripdetails.allow_guests') }} {{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='tripdetails.allow_guests') }}
{{ macros::checkbox(label='Immer anzeigen', name='tripdetails.always_show') }} {{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
{{ macros::input(label='Anmerkungen', name='tripdetails.notes', type='input') }} {{ macros::input(label='Anmerkungen', name='tripdetails.notes', type='input') }}
{{ macros::select(label='Typ', data=trip_types, name='tripdetails.trip_type', default='Reguläre Ausfahrt') }} {{ macros::select(label='Typ', data=trip_types, name='tripdetails.trip_type', default='Reguläre Ausfahrt') }}
<input value="Erstellen" class="w-full btn btn-primary" type="submit" /> <input value="Erstellen" class="w-full btn btn-primary" type="submit" />

View File

@ -5,7 +5,6 @@
{{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }} {{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }}
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }} {{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }} {{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
{{ macros::input(label='Anmerkungen', name='notes', type='input') }} {{ macros::input(label='Anmerkungen', name='notes', type='input') }}
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }} {{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
<input value="Erstellen" class="w-full btn btn-primary" type="submit" /> <input value="Erstellen" class="w-full btn btn-primary" type="submit" />

View File

@ -67,7 +67,8 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<div id="{{ day.day| date(format="%Y-%m-%d") }}" class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js" <div id="{{ day.day| date(format="%Y-%m-%d") }}"
class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
style="min-height: 10rem" style="min-height: 10rem"
data-trips="{{ amount_trips }}" data-trips="{{ amount_trips }}"
data-month="{{ day.day| date(format='%m') }}" data-month="{{ day.day| date(format='%m') }}"
@ -346,7 +347,6 @@
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3"> <form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }} {{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }} {{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }} {{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }} {{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
<input value="Speichern" class="btn btn-primary" type="submit" /> <input value="Speichern" class="btn btn-primary" type="submit" />
@ -369,7 +369,6 @@
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid"> <form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
{{ macros::input(label='', name='max_people', type='hidden', value=0) }} {{ macros::input(label='', name='max_people', type='hidden', value=0) }}
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }} {{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
{{ macros::input(label='', name='always_show', type='hidden', value=trip.always_show) }}
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }} {{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }} {{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" /> <input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
@ -379,6 +378,20 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{# --- END Edit Form --- #} {# --- END Edit Form --- #}
{# --- START Admin Form --- #}
{% if allowed_to_update_always_show_trip %}
<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">Admin-Modus</h3>
<form action="/cox/trip/{{ trip.id }}/toggle-always-show"
method="get"
class="grid gap-3">
<input value="{% if trip.always_show %}Normal anzeigen{% else %}Immer anzeigen{% endif %}"
class="btn btn-primary"
type="submit" />
</form>
</div>
{% endif %}
{# --- END Admin Form --- #}
</div> </div>
</div> </div>
{# --- END Sidebar Content --- #} {# --- END Sidebar Content --- #}